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..80bdc1fc5 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -395,12 +395,16 @@ } } - @if (canPayWithCashapp) { -
-

Pay  with

- -
- } +
+

Pay  with

+ @if (canPayWithCashapp) { + + } + @if (canPayWithApplePay) { + @if (canPayWithCashapp) {
} + + } +
} @@ -421,7 +425,7 @@ - } @else if (step === 'cashapp') { + } @else if (step === 'cashapp' || step === 'applepay') {
@@ -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...
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 49b12bbee..d73a81bb2 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 } from '../../shared/common.utils'; +import { md5, nextRoundNumber } from '../../shared/common.utils'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; import { ETA, EtaService } from '../../services/eta.service'; @@ -46,7 +46,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', @@ -60,6 +60,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; + @Input() applePayEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -109,11 +110,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; @@ -212,6 +214,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingCashapp = true; this.insertSquare(); this.setupSquare(); + } else if (this._step === 'applepay' && this.applePayEnabled) { + this.loadingApplePay = true; + this.insertSquare(); + this.setupSquare(); } else if (this._step === 'paid') { this.timePaid = Date.now(); this.timeoutTimer = setTimeout(() => { @@ -229,8 +235,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); @@ -421,17 +427,112 @@ export class AccelerateCheckout implements OnInit, OnDestroy { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) - await this.requestCashAppPayment(); + if (this._step === 'cashapp') { + 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(); } @@ -449,7 +550,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}`, @@ -467,23 +568,22 @@ 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: () => { - 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'); @@ -494,7 +594,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 @@ -597,6 +697,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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; + } + get canPayWithBalance() { if (!this.hasAccessToBalanceMode) { return false; 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 28e510e14..88ed5d8a9 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -181,4 +181,48 @@ export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): Mempo acc: !!tx[3], })) }; +} + +// 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +