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
+
+