From 920f225e6ccf48b2b7cf6aeb71ccb6b412d6b347 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 22:17:47 +0200 Subject: [PATCH 1/6] [accelerator] add support for acceleration with apple pay --- .../accelerate-checkout.component.html | 28 ++-- .../accelerate-checkout.component.scss | 18 +- .../accelerate-checkout.component.ts | 154 +++++++++++++++--- .../src/app/services/services-api.service.ts | 4 + frontend/src/app/shared/common.utils.ts | 44 +++++ frontend/src/resources/apple-pay.svg | 84 ++++++++++ 6 files changed, 301 insertions(+), 31 deletions(-) create mode 100755 frontend/src/resources/apple-pay.svg 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 08d3beed724e7758ea4e5bb40e51cf0b79bb5799 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 22:38:49 +0200 Subject: [PATCH 2/6] [accelerator] on mobile, autoscroll after selection cashapp or applepay --- .../accelerate-checkout/accelerate-checkout.component.html | 4 ++-- .../accelerate-checkout/accelerate-checkout.component.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) 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 80bdc1fc5..a8d799837 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -427,7 +427,7 @@
} @else if (step === 'cashapp' || step === 'applepay') { -
+

Confirm your payment

@@ -557,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.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index d73a81bb2..0722160b6 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -214,10 +214,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(() => { From 09b09710e47acaa4f8ca64931cb7511511702b3e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:07:55 +0200 Subject: [PATCH 3/6] [accelerator] fix cashapp acceleration on mobile --- .../accelerate-checkout/accelerate-checkout.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0722160b6..cfcdfb874 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -429,7 +429,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { try { //@ts-ignore this.payments = window.Square.payments(this.square.appId, this.square.locationId) - if (this._step === 'cashapp') { + 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(); From 3f7a24fb52ef6cf205ffe5187fe1cdd5521dc84f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:08:08 +0200 Subject: [PATCH 4/6] [accelerator] only show apple pay if available --- .../accelerate-checkout/accelerate-checkout.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 cfcdfb874..4552a81f7 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -60,7 +60,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() eta: ETA; @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; - @Input() applePayEnabled: boolean = true; + @Input() applePayEnabled: boolean = false; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -133,6 +133,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private enterpriseService: EnterpriseService, ) { this.accelerationUUID = window.crypto.randomUUID(); + + // 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() { From 8762ccaa09c00d70fe2301f5c2baed6ca5aa774f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 21 Jul 2024 23:22:11 +0200 Subject: [PATCH 5/6] [accelerator] remove attempt to align fiat payment methods --- .../accelerate-checkout/accelerate-checkout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a8d799837..58fd45356 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -395,7 +395,7 @@
} } -
+

Pay  with

@if (canPayWithCashapp) { From 4d44ee55fcfe97e978daef537737f2a7c27da053 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 24 Jul 2024 22:20:52 +0200 Subject: [PATCH 6/6] [accelerator] add missing getters for applepay --- .../accelerate-checkout.component.html | 2 +- .../accelerate-checkout.component.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) 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 58fd45356..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,7 +389,7 @@
}
- @if (canPayWithCashapp) { + @if (canPayWithCashapp || canPayWithApplePay) {

OR

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 632a7b127..71c46e2da 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -679,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; @@ -687,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() { @@ -707,7 +714,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return true; } } - + return false; } @@ -723,7 +730,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return true; } } - + return false; } @@ -736,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() {