diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html index 70d9ef3f3..0005c5c29 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html @@ -206,7 +206,7 @@ - + @@ -229,13 +229,21 @@ -
+
+ +
+
+
+ Loading +
+
+
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts index 3e8dbb6ff..219b68f88 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts @@ -1,5 +1,4 @@ import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; -import { ApiService } from '../../services/api.service'; import { Subscription, catchError, of, tap } from 'rxjs'; import { StorageService } from '../../services/storage.service'; import { Transaction } from '../../interfaces/electrs.interface'; @@ -40,7 +39,7 @@ export const MAX_BID_RATIO = 4; templateUrl: 'accelerate-preview.component.html', styleUrls: ['accelerate-preview.component.scss'] }) -export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { +export class AcceleratePreviewComponent implements OnDestroy, OnChanges { @Input() tx: Transaction | undefined; @Input() scrollEvent: boolean; @@ -63,18 +62,38 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges maxRateOptions: RateOption[] = []; + // Cashapp payment + paymentType: 'bitcoin' | 'cashapp' = 'bitcoin'; + cashAppSubscription: Subscription; + conversionsSubscription: Subscription; + payments: any; + showSpinner = false; + square: any; + cashAppPay: any; + hideCashApp = false; + constructor( public stateService: StateService, private servicesApiService: ServicesApiServices, private storageService: StorageService, private audioService: AudioService, private cd: ChangeDetectorRef - ) { } + ) { + if (window.document.referrer === 'cash.app') { + this.insertSquare(); + this.paymentType = 'cashapp'; + } else { + this.paymentType = 'bitcoin'; + } + } ngOnDestroy(): void { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } + if (this.cashAppPay) { + this.cashAppPay.destroy(); + } } ngOnChanges(changes: SimpleChanges): void { @@ -83,69 +102,85 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges } } - ngOnInit() { + ngAfterViewInit() { + this.showSpinner = true; + this.user = this.storageService.getAuth()?.user ?? null; - this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( - tap((response) => { - if (response.status === 204) { - this.estimate = undefined; - this.error = `cannot_accelerate_tx`; - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - this.estimateSubscription.unsubscribe(); - } else { - this.estimate = response.body; - if (!this.estimate) { + this.servicesApiService.setupSquare$().subscribe(ids => { + this.square = { + appId: ids.squareAppId, + locationId: ids.squareLocationId + }; + this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( + tap((response) => { + if (response.status === 204) { + this.estimate = undefined; this.error = `cannot_accelerate_tx`; this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.estimateSubscription.unsubscribe(); - } - - if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { - if (this.isLoggedIn()) { - this.error = `not_enough_balance`; + } else { + this.estimate = response.body; + if (!this.estimate) { + this.error = `cannot_accelerate_tx`; this.scrollToPreviewWithTimeout('mempoolError', 'center'); + this.estimateSubscription.unsubscribe(); + } + + if (this.paymentType === 'cashapp') { + this.estimate.userBalance = 999999999; + this.estimate.enoughBalance = true; + } + + if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { + if (this.isLoggedIn()) { + this.error = `not_enough_balance`; + this.scrollToPreviewWithTimeout('mempoolError', 'center'); + } + } + + this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; + + // Make min extra fee at least 50% of the current tx fee + this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); + + this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { + return { + fee: this.minExtraCost * multiplier, + rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize, + index, + }; + }); + + this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO; + this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO; + this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO; + + this.userBid = this.defaultBid; + if (this.userBid < this.minBidAllowed) { + this.userBid = this.minBidAllowed; + } else if (this.userBid > this.maxBidAllowed) { + this.userBid = this.maxBidAllowed; + } + this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + + if (!this.error) { + this.scrollToPreview('acceleratePreviewAnchor', 'start'); + if (this.paymentType === 'cashapp') { + this.setupSquare(); + } } } - - this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; - - // Make min extra fee at least 50% of the current tx fee - this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); - - this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { - return { - fee: this.minExtraCost * multiplier, - rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize, - index, - }; - }); - - this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO; - this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO; - this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO; - - this.userBid = this.defaultBid; - if (this.userBid < this.minBidAllowed) { - this.userBid = this.minBidAllowed; - } else if (this.userBid > this.maxBidAllowed) { - this.userBid = this.maxBidAllowed; - } - this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; - - if (!this.error) { - this.scrollToPreview('acceleratePreviewAnchor', 'start'); - } - } - }), - catchError((response) => { - this.estimate = undefined; - this.error = response.error; - this.scrollToPreviewWithTimeout('mempoolError', 'center'); - this.estimateSubscription.unsubscribe(); - return of(null); - }) - ).subscribe(); + }), + catchError((response) => { + this.estimate = undefined; + this.error = response.error; + this.scrollToPreviewWithTimeout('mempoolError', 'center'); + this.estimateSubscription.unsubscribe(); + return of(null); + }) + ).subscribe(); + }); } /** @@ -216,4 +251,112 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges onResize(): void { this.isMobile = window.innerWidth <= 767.98; } + + /** + * CashApp payment + */ + setupSquare() { + const init = () => { + this.initSquare(); + }; + + //@ts-ignore + if (!window.Square) { + console.warn('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() { + if (this.cashAppSubscription) { + this.cashAppSubscription.unsubscribe(); + } + if (this.conversionsSubscription) { + this.conversionsSubscription.unsubscribe(); + } + this.hideCashApp = false; + + + this.conversionsSubscription = this.stateService.conversions$.subscribe( + async (conversions) => { + const maxCostUsd = this.maxCost / 100_000_000 * conversions.USD; + const paymentRequest = this.payments.paymentRequest({ + countryCode: 'US', + currencyCode: 'USD', + total: { + amount: maxCostUsd.toString(), + label: 'Total', + pending: true, + productUrl: `https://mempool.space/tx/${this.tx.txid}`, + } + }); + this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { + redirectURL: `https://mempool.space/tx/${this.tx.txid}`, + referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + }); + await this.cashAppPay.attach('#cash-app-pay'); + this.showSpinner = 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.hideCashApp = true; + + that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$( + that.tx.txid, + that.userBid, + tokenResult.token, + tokenResult.details.cashAppPay.cashtag, + tokenResult.details.cashAppPay.referenceId + ).subscribe({ + next: () => { + that.audioService.playSound('ascend-chime-cartoon'); + that.showSuccess = true; + that.scrollToPreviewWithTimeout('successAlert', 'center'); + that.estimateSubscription.unsubscribe(); + }, + error: (response) => { + if (response.status === 403 && response.error === 'not_available') { + that.error = 'waitlisted'; + } else { + that.error = response.error; + } + that.scrollToPreviewWithTimeout('mempoolError', 'center'); + } + }); + } + }); + } + ); + } + + insertSquare(): void { + let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js'; + if (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); + })(); + } } diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 6f376f923..f3472f204 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -7,7 +7,6 @@ import { EnterpriseService } from '../../services/enterprise.service'; import { NavigationService } from '../../services/navigation.service'; import { MenuComponent } from '../menu/menu.component'; import { StorageService } from '../../services/storage.service'; -import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-master-page', @@ -45,7 +44,6 @@ export class MasterPageComponent implements OnInit, OnDestroy { private enterpriseService: EnterpriseService, private navigationService: NavigationService, private storageService: StorageService, - private apiService: ApiService, private router: Router, ) { } diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 0caa06168..4a8314e4b 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -132,6 +132,10 @@ export class ServicesApiServices { return this.httpClient.post(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid }); } + accelerateWithCashApp$(txInput: string, userBid: number, token: string, cashtag: string, referenceId: string) { + return this.httpClient.post(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, userBid: userBid, token: token, cashtag: cashtag, referenceId: referenceId }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations`); } @@ -151,4 +155,8 @@ export class ServicesApiServices { getAccelerationStats$(): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`); } + + setupSquare$(): Observable<{squareAppId: string, squareLocationId: string}> { + return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${SERVICES_API_PREFIX}/square/setup`); + } }