diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index aa45d7bd5..27786bd9b 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -389,18 +389,22 @@ } - @if (canPayWithCashapp) { + @if (canPayWithCashapp || canPayWithApplePay) {

OR

} } - @if (canPayWithCashapp) { -
-

Pay  with

- -
- } +
+

Pay  with

+ @if (canPayWithCashapp) { + + } + @if (canPayWithApplePay) { + @if (canPayWithCashapp) {
} + + } +
} @@ -421,9 +425,9 @@ - } @else if (step === 'cashapp') { + } @else if (step === 'cashapp' || step === 'applepay') { -
+

Confirm your payment

@@ -437,7 +441,7 @@
- @if (!loadingCashapp) { + @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
@@ -456,8 +460,12 @@
-
- @if (loadingCashapp) { + @if (step === 'applepay') { +
+ } @else if (step === 'cashapp') { +
+ } + @if (loadingCashapp || loadingApplePay) {
Loading payment method...
@@ -549,7 +557,7 @@ -Accelerate to ~{{ x | number : '1.0-0' }} sat/vB +Accelerate to ~{{ x | number : '1.0-0' }} sat/vB
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index 4e7be2691..b35308384 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -11,8 +11,7 @@ .paymentMethod { padding: 10px; background-color: var(--secondary); - border-radius: 15px; - border: 2px solid var(--bg); + border-radius: 10px; cursor: pointer; } @@ -202,4 +201,19 @@ .btn-error-wrapper { height: 26px; +} + +.apple-pay-button { + display: inline-block; + -webkit-appearance: -apple-pay-button; + -apple-pay-button-type: plain; /* Use any supported button type. */ +} +.apple-pay-button-black { + -apple-pay-button-style: black; +} +.apple-pay-button-white { + -apple-pay-button-style: white; +} +.apple-pay-button-white-with-line { + -apple-pay-button-style: white-outline; } \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 185238f72..71c46e2da 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { ServicesApiServices } from '../../services/services-api.service'; -import { nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils'; +import { md5, nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; import { ETA, EtaService } from '../../services/eta.service'; @@ -47,7 +47,7 @@ export const MIN_BID_RATIO = 1; export const DEFAULT_BID_RATIO = 2; export const MAX_BID_RATIO = 4; -type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid' | 'success'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'processing' | 'paid' | 'success'; @Component({ selector: 'app-accelerate-checkout', @@ -61,6 +61,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; + @Input() applePayEnabled: boolean = false; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -110,11 +111,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // square loadingCashapp = false; + loadingApplePay = false; cashappError = false; cashappSubmit: any; payments: any; cashAppPay: any; - cashAppSubscription: Subscription; + applePay: any; conversionsSubscription: Subscription; conversions: any; @@ -133,6 +135,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private enterpriseService: EnterpriseService, ) { this.accelerationUUID = insecureRandomUUID(); + + // Check if Apple Pay available + // @ts-ignore https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview + if (window.ApplePaySession) { + this.applePayEnabled = true; + } } ngOnInit() { @@ -214,6 +222,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingCashapp = true; this.insertSquare(); this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); + } else if (this._step === 'applepay' && this.applePayEnabled) { + this.loadingApplePay = true; + this.insertSquare(); + this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'paid') { this.timePaid = Date.now(); this.timeoutTimer = setTimeout(() => { @@ -231,8 +245,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } /** - * Scroll to element id with or without setTimeout - */ + * Scroll to element id with or without setTimeout + */ scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { setTimeout(() => { this.scrollToElement(id, position); @@ -424,17 +438,113 @@ export class AccelerateCheckout implements OnInit, OnDestroy { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) - await this.requestCashAppPayment(); + const urlParams = new URLSearchParams(window.location.search); + if (this._step === 'cashapp' || urlParams.get('cash_request_id')) { + await this.requestCashAppPayment(); + } else if (this._step === 'applepay') { + await this.requestApplePayPayment(); + } } catch (e) { console.debug('Error loading Square Payments', e); this.cashappError = true; return; } } - async requestCashAppPayment() { - if (this.cashAppSubscription) { - this.cashAppSubscription.unsubscribe(); + + /** + * APPLE PAY + */ + async requestApplePayPayment() { + if (this.conversionsSubscription) { + this.conversionsSubscription.unsubscribe(); } + + this.conversionsSubscription = this.stateService.conversions$.subscribe( + async (conversions) => { + this.conversions = conversions; + if (this.applePay) { + this.applePay.destroy(); + } + + const costUSD = this.cost / 100_000_000 * conversions.USD; + const paymentRequest = this.payments.paymentRequest({ + countryCode: 'US', + currencyCode: 'USD', + total: { + amount: costUSD.toFixed(2), + label: 'Total', + }, + }); + + try { + this.applePay = await this.payments.applePay(paymentRequest); + const applePayButton = document.getElementById('apple-pay-button'); + if (!applePayButton) { + console.error(`Unable to find apple pay button id='apple-pay-button'`); + // Try again + setTimeout(this.requestApplePayPayment.bind(this), 500); + return; + } + this.loadingApplePay = false; + applePayButton.addEventListener('click', async event => { + event.preventDefault(); + const tokenResult = await this.applePay.tokenize(); + if (tokenResult?.status === 'OK') { + const card = tokenResult.details?.card; + if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { + console.error(`Cannot retreive payment card details`); + this.accelerateError = 'apple_pay_no_card_details'; + return; + } + const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + this.servicesApiService.accelerateWithApplePay$( + this.tx.txid, + tokenResult.token, + cardTag, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + this.accelerationUUID + ).subscribe({ + next: () => { + this.audioService.playSound('ascend-chime-cartoon'); + if (this.applePay) { + this.applePay.destroy(); + } + setTimeout(() => { + this.moveToStep('paid'); + }, 1000); + }, + error: (response) => { + this.accelerateError = response.error; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + } else { + let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors, + )}`; + } + throw new Error(errorMessage); + } + }); + } catch (e) { + console.error(e); + } + } + ); + } + + /** + * CASHAPP + */ + async requestCashAppPayment() { if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } @@ -452,7 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { countryCode: 'US', currencyCode: 'USD', total: { - amount: costUSD.toString(), + amount: costUSD.toFixed(2), label: 'Total', pending: true, productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, @@ -470,24 +580,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.loadingCashapp = false; - const that = this; - this.cashAppPay.addEventListener('ontokenization', function (event) { + this.cashAppPay.addEventListener('ontokenization', event => { const { tokenResult, error } = event.detail; if (error) { this.accelerateError = error; } else if (tokenResult.status === 'OK') { - that.servicesApiService.accelerateWithCashApp$( - that.tx.txid, + this.servicesApiService.accelerateWithCashApp$( + this.tx.txid, tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, - that.accelerationUUID + this.accelerationUUID ).subscribe({ next: () => { this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); - that.audioService.playSound('ascend-chime-cartoon'); - if (that.cashAppPay) { - that.cashAppPay.destroy(); + this.audioService.playSound('ascend-chime-cartoon'); + if (this.cashAppPay) { + this.cashAppPay.destroy(); } setTimeout(() => { this.moveToStep('paid'); @@ -498,7 +607,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { - that.accelerateError = response.error; + this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { // Reset everything by reloading the page :D, can be improved @@ -570,6 +679,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return !!this.estimate?.availablePaymentMethods?.cashapp; } + get couldPayWithApplePay() { + if (!this.applePayEnabled) { + return false; + } + return !!this.estimate?.availablePaymentMethods?.applePay; + } + get couldPayWithBalance() { if (!this.hasAccessToBalanceMode) { return false; @@ -578,7 +694,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get couldPay() { - return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp; + return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay; } get canPayWithBitcoin() { @@ -598,7 +714,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return true; } } - + + return false; + } + + get canPayWithApplePay() { + if (!this.applePayEnabled || !this.conversions) { + return false; + } + + const paymentMethod = this.estimate?.availablePaymentMethods?.applePay; + if (paymentMethod) { + const costUSD = (this.cost / 100_000_000 * this.conversions.USD); + if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) { + return true; + } + } + return false; } @@ -611,7 +743,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPay() { - return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; + return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay; } get hasAccessToBalanceMode() { diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bfd7da81b..c26075198 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -137,6 +137,10 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); } + accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`); } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index e22754d27..697b11b5e 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -195,3 +195,47 @@ export function insecureRandomUUID(): string { } return uuid.slice(0, -1); } + +// https://stackoverflow.com/a/60467595 +export function md5(inputString): string { + var hc="0123456789abcdef"; + function rh(n) {var j,s="";for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;} + function ad(x,y) {var l=(x&0xFFFF)+(y&0xFFFF);var m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);} + function rl(n,c) {return (n<>>(32-c));} + function cm(q,a,b,x,s,t) {return ad(rl(ad(ad(a,q),ad(x,t)),s),b);} + function ff(a,b,c,d,x,s,t) {return cm((b&c)|((~b)&d),a,b,x,s,t);} + function gg(a,b,c,d,x,s,t) {return cm((b&d)|(c&(~d)),a,b,x,s,t);} + function hh(a,b,c,d,x,s,t) {return cm(b^c^d,a,b,x,s,t);} + function ii(a,b,c,d,x,s,t) {return cm(c^(b|(~d)),a,b,x,s,t);} + function sb(x) { + var i;var nblk=((x.length+8)>>6)+1;var blks=new Array(nblk*16);for(i=0;i>2]|=x.charCodeAt(i)<<((i%4)*8); + blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks; + } + var i,x=sb(""+inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd; + for(i=0;i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +