0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) {
@@ -361,7 +361,7 @@
- @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
+ @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
Your account will be debited no more than {{ cost | number }} sats
@@ -484,6 +484,11 @@
}
+ @if (isTokenizing > 0) {
+
+ }
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 ad085ed20..75c6a397d 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
@@ -8,6 +8,13 @@
color: var(--green)
}
+.accelerate-checkout-inner {
+ &.input-disabled {
+ pointer-events: none;
+ opacity: 0.75;
+ }
+}
+
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
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 4c935c57f..34a98b8dc 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
@@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
calculating = true;
processing = false;
+ isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
+ isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@@ -154,7 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
- this.moveToStep('summary');
+ this.moveToStep('summary', true);
} else {
this.auth = auth;
}
@@ -163,11 +165,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
- this.moveToStep('processing');
+ this.moveToStep('processing', true);
this.insertSquare();
this.setupSquare();
} else {
- this.moveToStep('summary');
+ this.moveToStep('summary', true);
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
@@ -192,14 +194,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
- this.moveToStep('success');
+ this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
}
}
- moveToStep(step: CheckoutStep): void {
+ moveToStep(step: CheckoutStep, force: boolean = false): void {
+ if (this.isCheckoutLocked > 0 && !force) {
+ return;
+ }
this.processing = false;
this._step = step;
if (this.timeoutTimer) {
@@ -242,7 +247,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void {
this.completed.emit(true);
- this.moveToStep('summary');
+ this.moveToStep('summary', true);
}
/**
@@ -393,7 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
- this.moveToStep('paid');
+ this.moveToStep('paid', true);
},
error: (response) => {
this.processing = false;
@@ -503,56 +508,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
+ if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
+ return;
+ }
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';
- this.processing = false;
- 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)}`,
- costUSD
- ).subscribe({
- next: () => {
+ try {
+ // lock the checkout UI and show a loading spinner until the square modals are finished
+ this.isCheckoutLocked++;
+ this.isTokenizing++;
+ 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';
this.processing = false;
- this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
- this.audioService.playSound('ascend-chime-cartoon');
- if (this.applePay) {
- this.applePay.destroy();
- }
- setTimeout(() => {
- this.moveToStep('paid');
- }, 1000);
- },
- error: (response) => {
- this.processing = false;
- 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);
- }
+ return;
}
- });
- } else {
- this.processing = false;
- let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
- if (tokenResult.errors) {
- errorMessage += ` and errors: ${JSON.stringify(
- tokenResult.errors,
- )}`;
+ const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
+ // keep checkout in loading state until the acceleration request completes
+ this.isTokenizing++;
+ this.isCheckoutLocked++;
+ this.servicesApiService.accelerateWithApplePay$(
+ this.tx.txid,
+ tokenResult.token,
+ cardTag,
+ `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
+ costUSD
+ ).subscribe({
+ next: () => {
+ this.processing = false;
+ this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
+ this.audioService.playSound('ascend-chime-cartoon');
+ if (this.applePay) {
+ this.applePay.destroy();
+ }
+ setTimeout(() => {
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
+ this.moveToStep('paid', true);
+ }, 1000);
+ },
+ error: (response) => {
+ this.processing = false;
+ this.accelerateError = response.error;
+ if (!(response.status === 403 && response.error === 'not_available')) {
+ setTimeout(() => {
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
+ // 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 {
+ this.processing = false;
+ let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
+ if (tokenResult.errors) {
+ errorMessage += ` and errors: ${JSON.stringify(
+ tokenResult.errors,
+ )}`;
+ }
+ throw new Error(errorMessage);
}
- throw new Error(errorMessage);
+ } finally {
+ // always unlock the checkout once we're finished
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
}
});
} catch (e) {
@@ -602,65 +626,84 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
+ if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
+ return;
+ }
event.preventDefault();
- const tokenResult = await this.googlePay.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';
- this.processing = false;
- return;
- }
- const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
- if (!verificationToken || !verificationToken.token) {
- console.error(`SCA verification failed`);
- this.accelerateError = 'SCA Verification Failed. Payment Declined.';
- this.processing = false;
- return;
- }
- const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
- this.servicesApiService.accelerateWithGooglePay$(
- this.tx.txid,
- tokenResult.token,
- verificationToken.token,
- cardTag,
- `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
- costUSD,
- verificationToken.userChallenged
- ).subscribe({
- next: () => {
+ try {
+ // lock the checkout UI and show a loading spinner until the square modals are finished
+ this.isCheckoutLocked++;
+ this.isTokenizing++;
+ const tokenResult = await this.googlePay.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';
this.processing = false;
- this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
- this.audioService.playSound('ascend-chime-cartoon');
- if (this.googlePay) {
- this.googlePay.destroy();
- }
- setTimeout(() => {
- this.moveToStep('paid');
- }, 1000);
- },
- error: (response) => {
- this.processing = false;
- 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);
- }
+ return;
}
- });
- } else {
- this.processing = false;
- let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
- if (tokenResult.errors) {
- errorMessage += ` and errors: ${JSON.stringify(
- tokenResult.errors,
- )}`;
+ const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
+ if (!verificationToken || !verificationToken.token) {
+ console.error(`SCA verification failed`);
+ this.accelerateError = 'SCA Verification Failed. Payment Declined.';
+ this.processing = false;
+ return;
+ }
+ const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
+ // keep checkout in loading state until the acceleration request completes
+ this.isCheckoutLocked++;
+ this.isTokenizing++;
+ this.servicesApiService.accelerateWithGooglePay$(
+ this.tx.txid,
+ tokenResult.token,
+ verificationToken.token,
+ cardTag,
+ `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
+ costUSD,
+ verificationToken.userChallenged
+ ).subscribe({
+ next: () => {
+ this.processing = false;
+ this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
+ this.audioService.playSound('ascend-chime-cartoon');
+ if (this.googlePay) {
+ this.googlePay.destroy();
+ }
+ setTimeout(() => {
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
+ this.moveToStep('paid', true);
+ }, 1000);
+ },
+ error: (response) => {
+ this.processing = false;
+ this.accelerateError = response.error;
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
+ 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 {
+ this.processing = false;
+ let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
+ if (tokenResult.errors) {
+ errorMessage += ` and errors: ${JSON.stringify(
+ tokenResult.errors,
+ )}`;
+ }
+ throw new Error(errorMessage);
}
- throw new Error(errorMessage);
+ } finally {
+ // always unlock the checkout once we're finished
+ this.isTokenizing--;
+ this.isCheckoutLocked--;
}
});
}
@@ -727,7 +770,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.destroy();
}
setTimeout(() => {
- this.moveToStep('paid');
+ this.moveToStep('paid', true);
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@@ -801,7 +844,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
- this.moveToStep('paid');
+ this.moveToStep('paid', true);
}
isLoggedIn(): boolean {