import { Component, OnInit, OnDestroy, Input } from '@angular/core'; import { Subscription, tap, of, catchError } from 'rxjs'; import { WebsocketService } from '../../services/websocket.service'; import { ServicesApiServices } from '../../services/services-api.service'; import { nextRoundNumber } from '../../shared/common.utils'; import { StateService } from '../../services/state.service'; @Component({ selector: 'app-accelerate-checkout', templateUrl: './accelerate-checkout.component.html', styleUrls: ['./accelerate-checkout.component.scss'] }) export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: number = Date.now() + 123456789; @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad'; calculating = true; choosenOption: 'wait' | 'accelerate' = 'wait'; showCheckoutPage = false; error = ''; // accelerator stuff square: { appId: string, locationId: string}; accelerationUUID: string; estimateSubscription: Subscription; cost: number; // sats // square cashappSubmit: any; payments: any; cashAppPay: any; cashAppSubscription: Subscription; conversionsSubscription: Subscription; loadingCashapp = true; processingPayment = true; constructor( private websocketService: WebsocketService, private servicesApiService: ServicesApiServices, private stateService: StateService ) {} ngOnInit() { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { // Redirected from cashapp this.processingPayment = true; window.scrollTo(0, 0); } else { this.servicesApiService.setupSquare$().subscribe(ids => { this.square = { appId: ids.squareAppId, locationId: ids.squareLocationId }; this.estimate(); }); } } ngOnDestroy() { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } } /** * Accelerator */ estimate() { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } this.calculating = true; this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe( tap((response) => { this.calculating = false; if (response.status === 204) { this.error = `cannot_accelerate_tx`; } else { const estimation = response.body; if (!estimation) { this.error = `cannot_accelerate_tx`; return; } // Make min extra fee at least 50% of the current tx fee const minExtraCost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee)); const DEFAULT_BID_RATIO = 2; this.cost = minExtraCost * DEFAULT_BID_RATIO + estimation.mempoolBaseFee + estimation.vsizeFee; } }), catchError((response) => { this.error = `cannot_accelerate_tx`; return of(null); }) ).subscribe(); } /** * Square */ insertSquare(): void { //@ts-ignore if (window.Square) { return; } let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js'; if (document.location.hostname === 'mempool-staging.fmt.mempool.space' || document.location.hostname === 'mempool-staging.va1.mempool.space' || document.location.hostname === 'mempool-staging.fra.mempool.space' || document.location.hostname === 'mempool-staging.tk7.mempool.space' || document.location.hostname === 'mempool.space') { statsUrl = 'https://web.squarecdn.com/v1/square.js'; } (function() { const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; // @ts-ignore g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s); })(); } setupSquare() { const init = () => { this.initSquare(); }; //@ts-ignore if (!window.Square) { console.debug('Square.js failed to load properly. Retrying in 1 second.'); setTimeout(init, 1000); } else { init(); } } async initSquare(): Promise { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) await this.requestCashAppPayment(); } catch (e) { console.error(e); this.error = 'Error loading Square Payments'; return; } } async requestCashAppPayment() { this.loadingCashapp = true; if (this.cashAppSubscription) { this.cashAppSubscription.unsubscribe(); } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { if (this.cashAppPay) { this.cashAppPay.destroy(); } const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`; const costUSD = this.cost / 100_000_000 * conversions.USD; const paymentRequest = this.payments.paymentRequest({ countryCode: 'US', currencyCode: 'USD', total: { amount: costUSD.toString(), label: 'Total', pending: true, productUrl: `${redirectHostname}/tracker/${this.txid}`, }, button: { shape: 'semiround', size: 'small', theme: 'light'} }); this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { redirectURL: `${redirectHostname}/tracker/${this.txid}?acceleration=false`, referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, button: { shape: 'semiround', size: 'small', theme: 'light'} }); this.cashappSubmit = await this.cashAppPay.CashAppPayInstance.render('#cash-app-pay', { button: { theme: 'light', size: 'small', shape: 'semiround' }, manage: false }); const that = this; this.cashAppPay.addEventListener('ontokenization', function (event) { const { tokenResult, error } = event.detail; if (error) { this.error = error; } else if (tokenResult.status === 'OK') { that.servicesApiService.accelerateWithCashApp$( that.txid, that.cost, tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, that.accelerationUUID ).subscribe({ next: () => { that.estimateSubscription.unsubscribe(); }, error: (response) => { if (response.status === 403 && response.error === 'not_available') { that.error = 'waitlisted'; } else { that.error = response.error; } } }); } }); this.loadingCashapp = false; } ); } submitCashappPay(): void { if (this.cashappSubmit) { this.cashappSubmit?.begin(); this.processingPayment = true; } } /** * UI events */ enableCheckoutPage() { this.showCheckoutPage = true; this.insertSquare(); this.setupSquare(); } selectedOptionChanged(event) { this.choosenOption = event.target.id; } restart() { this.showCheckoutPage = false this.choosenOption = 'wait'; } }