mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
faster accelerator checkout with square payment service
This commit is contained in:
parent
d5877c2d02
commit
3d2cf3008e
6 changed files with 1099 additions and 492 deletions
|
|
@ -29,6 +29,7 @@ import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten
|
|||
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { SquarePaymentService } from '@app/services/square-payment.service';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
const providers = [
|
||||
|
|
@ -53,6 +54,7 @@ const providers = [
|
|||
AppPreloadingStrategy,
|
||||
ServicesApiServices,
|
||||
PreloadService,
|
||||
SquarePaymentService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
|
||||
{ provide: ZONE_SERVICE, useClass: ZoneService },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -365,81 +365,6 @@
|
|||
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-area" style="font-size: 14px;">
|
||||
<div class="row text-center justify-content-center mx-2">
|
||||
<span i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></span>
|
||||
</div>
|
||||
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
|
||||
<div class="row">
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
||||
<div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || quoteError || accelerateError || showSuccess">
|
||||
<ng-container *ngTemplateOutlet="accountPayButton"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="row">
|
||||
@if (canPayWithBitcoin) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
@if (invoice) {
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p>
|
||||
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
|
||||
} @else if (btcpayInvoiceFailed) {
|
||||
<div class="btcpay-invoice">
|
||||
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
|
||||
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
|
||||
@if (!loadingBtcpayInvoice) {
|
||||
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
|
||||
<div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;">
|
||||
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
||||
<p class="text-nowrap">——<span i18n="or"> OR </span>——</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||
@if (canPayWithCashapp) {
|
||||
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
}
|
||||
@if (canPayWithApplePay) {
|
||||
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
|
||||
<img src="/resources/apple-pay.png" height=37>
|
||||
</div>
|
||||
}
|
||||
@if (canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
|
||||
<img src="/resources/google-pay.png" height=37>
|
||||
</div>
|
||||
}
|
||||
@if (canPayWithCardOnFile) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
|
||||
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
|
||||
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
|
||||
} @else {
|
||||
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
|
||||
}
|
||||
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingCheckout>
|
||||
<div class="row">
|
||||
|
|
@ -450,14 +375,90 @@
|
|||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<div class="checkout-payment-area" [class.visible]="step === 'checkout'">
|
||||
<div class="payment-area" style="font-size: 14px;">
|
||||
<div class="row text-center justify-content-center mx-2">
|
||||
<span i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx?.txid?.substr(0, 10) }}..{{ tx?.txid?.substr(-10) }}</a></span>
|
||||
</div>
|
||||
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
|
||||
<div class="row">
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
||||
<div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || quoteError || accelerateError || showSuccess">
|
||||
<ng-container *ngTemplateOutlet="accountPayButton"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="row">
|
||||
@if (canPayWithBitcoin) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
@if (invoice) {
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p>
|
||||
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
|
||||
} @else if (btcpayInvoiceFailed) {
|
||||
<div class="btcpay-invoice">
|
||||
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
|
||||
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
|
||||
@if (!loadingBtcpayInvoice) {
|
||||
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
|
||||
<div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;">
|
||||
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
||||
<p class="text-nowrap">——<span i18n="or"> OR </span>——</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center square-checkout-buttons">
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||
<div class="square-buttons" [class.loading]="!squarePaymentsReady">
|
||||
<div class="square-loading-overlay">
|
||||
<div class="spinner-border text-light" style="width: 25px; height: 25px;"></div>
|
||||
</div>
|
||||
<div id="cash-app-pay" class="square-button" [class.d-none]="!canPayWithCashapp || !availableMethods.cashapp"></div>
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black square-button" [class.d-none]="!canPayWithApplePay || !availableMethods.applepay"></div>
|
||||
<div id="google-pay-button" class="square-button" [class.d-none]="!canPayWithGooglePay || !availableMethods.googlepay"></div>
|
||||
@if (canPayWithCardOnFile) {
|
||||
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center card-on-file" (click)="moveToStep('cardonfile')">
|
||||
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
|
||||
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
|
||||
} @else {
|
||||
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
|
||||
}
|
||||
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (isTokenizing > 0) {
|
||||
<div class="d-flex flex-row justify-content-center">
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row mt-2 mb-2 text-center">
|
||||
<div class="col-sm d-flex flex-column">
|
||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
|
||||
</div>
|
||||
|
||||
@if (step === 'cardonfile') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
|
|
@ -473,7 +474,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
|
||||
@if (!loadingCardOnFile) {
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
|
|
@ -492,27 +493,19 @@
|
|||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@if (step === 'applepay') {
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'cashapp') {
|
||||
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'googlepay') {
|
||||
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'cardonfile') {
|
||||
<div class="ml-auto mr-auto paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
|
||||
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
|
||||
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
|
||||
} @else {
|
||||
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
|
||||
}
|
||||
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
|
||||
<div display="d-flex flex-row justify-content-center">
|
||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
<div class="ml-auto mr-auto paymentMethod mx-2 d-flex justify-content-center align-items-center card-on-file" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
|
||||
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
|
||||
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
|
||||
} @else {
|
||||
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
|
||||
}
|
||||
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||
</div>
|
||||
@if (loadingCardOnFile) {
|
||||
<div display="d-flex flex-row justify-content-center">
|
||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (isTokenizing > 0) {
|
||||
|
|
@ -530,7 +523,8 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@else if (step === 'processing') {
|
||||
|
||||
@if (step === 'processing') {
|
||||
<div class="row mb-1 text-center">
|
||||
<div class="col-sm">
|
||||
<h1 style="font-size: larger;"><ng-content select="[slot='processing-title']"></ng-content><span class="default-slot" i18n="accelerator.confirming-your-payment">Confirming your payment</span></h1>
|
||||
|
|
@ -541,7 +535,7 @@
|
|||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
<!-- Processing payment -->
|
||||
<div id="cash-app-pay" class="d-inline-block" [style]="'opacity: 0; width: 0px; height: 0px; pointer-events: none;'"></div>
|
||||
<div id="cash-app-pay-processing" class="d-inline-block" [style]="'opacity: 0; width: 0px; height: 0px; pointer-events: none;'"></div>
|
||||
<div display="d-flex flex-row justify-content-center">
|
||||
<span i18n="accelerator.payment-processing">We are processing your payment...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
|
|
@ -549,6 +543,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row mt-2 mb-2 text-center">
|
||||
<div class="col-sm d-flex flex-column">
|
||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@else if (step === 'paid') {
|
||||
<div class="row mb-1 text-center">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@
|
|||
background-color: var(--secondary);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.card-on-file {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
background: black;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(51, 51, 51);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-slot:not(:only-child) {
|
||||
|
|
@ -155,8 +165,9 @@
|
|||
background: var(--bg);
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0;
|
||||
@media (max-width: 575px) {
|
||||
padding-bottom: 1.25rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,4 +248,65 @@
|
|||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkout-payment-area {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
position: static;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.square-checkout-buttons {
|
||||
.square-buttons {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1em;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
#cash-app-pay {
|
||||
width: 182.5px;
|
||||
}
|
||||
|
||||
.square-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.square-buttons.loading .square-loading-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.square-button {
|
||||
width: 240px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
#google-pay-button, #cash-app-pay {
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
|
|||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { isDevMode } from '@angular/core';
|
||||
import { SquarePaymentService, SquareInitConfig } from '@app/services/square-payment.service';
|
||||
import { log } from '@app/shared/logger.utils';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
|
||||
|
||||
|
|
@ -49,7 +51,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' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cardonfile' | 'processing' | 'paid' | 'success';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-checkout',
|
||||
|
|
@ -77,6 +79,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
|
||||
calculating = true;
|
||||
processing = false;
|
||||
squarePaymentsReady = false;
|
||||
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
|
||||
isTokenizing = 0; // reference counter, 0 = false, >0 = true
|
||||
selectedOption: 'wait' | 'accel';
|
||||
|
|
@ -91,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
accelerationResponse: { receiptUrl: string | null } | undefined;
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
timeoutTimer: any;
|
||||
|
||||
authSubscription$: Subscription;
|
||||
|
|
@ -116,16 +118,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
// square
|
||||
loadingCashapp = false;
|
||||
loadingApplePay = false;
|
||||
loadingGooglePay = false;
|
||||
loadingCardOnFile = false;
|
||||
payments: any;
|
||||
cashAppPay: any;
|
||||
applePay: any;
|
||||
googlePay: any;
|
||||
conversionsSubscription: Subscription;
|
||||
conversions: Record<string, number>;
|
||||
availableMethods: Record<string, boolean> = {};
|
||||
firstLoad = true;
|
||||
|
||||
// btcpay
|
||||
loadingBtcpayInvoice = false;
|
||||
|
|
@ -140,6 +137,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
private cd: ChangeDetectorRef,
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private squarePaymentService: SquarePaymentService,
|
||||
) {
|
||||
this.isProdDomain = this.stateService.isProdDomain;
|
||||
|
||||
|
|
@ -169,19 +167,64 @@ 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', true);
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
} else {
|
||||
this.moveToStep('summary', true);
|
||||
}
|
||||
|
||||
this.registerSquareCallbacks();
|
||||
this.squarePaymentService.init();
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
if (this.step !== 'checkout') {
|
||||
this.updateSquarePayments();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerSquareCallbacks(): void {
|
||||
this.squarePaymentService.registerCallback('error', (error: any) => {
|
||||
console.error('Square Payment Error', error);
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.squarePaymentService.registerCallback('loading', () => {
|
||||
this.squarePaymentsReady = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.squarePaymentService.registerCallback('ready', (availableMethods: string[]) => {
|
||||
this.availableMethods = {};
|
||||
for (const method of availableMethods) {
|
||||
this.availableMethods[method] = true;
|
||||
}
|
||||
this.squarePaymentsReady = true;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.squarePaymentService.registerCallback('cashAppTokenized', this.onCashAppTokenized.bind(this));
|
||||
this.squarePaymentService.registerCallback('googlePayClicked', this.onGooglePayClicked.bind(this));
|
||||
this.squarePaymentService.registerCallback('applePayClicked', this.onApplePayClicked.bind(this));
|
||||
}
|
||||
|
||||
updateSquarePayments(): void {
|
||||
if (this.canPayWithGooglePay || this.canPayWithApplePay || this.canPayWithCashapp) {
|
||||
this.squarePaymentService.update(
|
||||
{
|
||||
availableMethods: {
|
||||
googlePay: this.canPayWithGooglePay,
|
||||
applePay: this.canPayWithApplePay,
|
||||
cashApp: this.canPayWithCashapp,
|
||||
},
|
||||
txid: this.tx.txid,
|
||||
costUSD: this.cost / 100_000_000 * this.conversions.USD,
|
||||
},
|
||||
this.step === 'checkout' || this.firstLoad,
|
||||
);
|
||||
this.firstLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
|
|
@ -189,6 +232,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
if (this.authSubscription$) {
|
||||
this.authSubscription$.unsubscribe();
|
||||
}
|
||||
this.squarePaymentService.cleanup();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
|
|
@ -210,7 +254,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.insertSquare();
|
||||
this.enterpriseService.goal(8);
|
||||
this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100);
|
||||
}
|
||||
|
|
@ -219,22 +262,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.invoice = undefined;
|
||||
this.requestBTCPayInvoice();
|
||||
this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100);
|
||||
} else if (this._step === 'cashapp') {
|
||||
this.loadingCashapp = true;
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
||||
this.loadingApplePay = true;
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
|
||||
this.loadingGooglePay = true;
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
|
||||
this.loadingCardOnFile = true;
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'paid') {
|
||||
this.timePaid = Date.now();
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
|
|
@ -348,7 +375,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}
|
||||
return of(null);
|
||||
})
|
||||
).subscribe();
|
||||
).subscribe(() => {
|
||||
this.updateSquarePayments();
|
||||
});
|
||||
}
|
||||
|
||||
validateChoice(): void {
|
||||
|
|
@ -374,6 +403,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.userBid = Math.max(0, fee);
|
||||
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
this.validateChoice();
|
||||
this.updateSquarePayments();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -407,299 +437,173 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Square
|
||||
*/
|
||||
insertSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
if (window['Square']) {
|
||||
return;
|
||||
}
|
||||
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||
if (this.isProdDomain) {
|
||||
statsUrl = '/square/v1/square.js';
|
||||
}
|
||||
|
||||
(function(): void {
|
||||
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
}
|
||||
setupSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
const init = (): void => {
|
||||
this.initSquare();
|
||||
};
|
||||
|
||||
if (!window['Square']) {
|
||||
console.debug('Square.js failed to load properly. Retrying.');
|
||||
setTimeout(this.setupSquare.bind(this), 100);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
async initSquare(): Promise<void> {
|
||||
try {
|
||||
this.servicesApiService.setupSquare$().subscribe({
|
||||
next: async (ids) => {
|
||||
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
|
||||
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();
|
||||
} else if (this._step === 'googlepay') {
|
||||
await this.requestGooglePayPayment();
|
||||
} else if (this._step === 'cardonfile') {
|
||||
this.loadingCardOnFile = false;
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
console.debug('Error loading Square Payments');
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.debug('Error loading Square Payments', e);
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
async onApplePayClicked(event: Event, applePay: any, config: SquareInitConfig): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
try {
|
||||
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||
this.isCheckoutLocked++;
|
||||
this.isTokenizing++;
|
||||
const tokenResult = await applePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retrieve payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const costUSD = this.cost / 100_000_000 * this.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);
|
||||
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)}`,
|
||||
config.costUSD
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
applePayButton.addEventListener('click', async event => {
|
||||
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
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 retrieve 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());
|
||||
// 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: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
}
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
} finally {
|
||||
// always unlock the checkout once we're finished
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.squarePaymentService.cleanup();
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.processing = false;
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
} finally {
|
||||
// always unlock the checkout once we're finished
|
||||
this.isTokenizing--;
|
||||
this.isCheckoutLocked--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GOOGLE PAY
|
||||
*/
|
||||
async requestGooglePayPayment(): Promise<void> {
|
||||
async onGooglePayClicked(event: Event, googlePay: any, config: SquareInitConfig): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
try {
|
||||
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||
this.isCheckoutLocked++;
|
||||
this.isTokenizing++;
|
||||
const tokenResult = await googlePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retrieve payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
|
||||
const paymentRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total'
|
||||
}
|
||||
});
|
||||
this.googlePay = await this.payments.googlePay(paymentRequest , {
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
});
|
||||
|
||||
await this.googlePay.attach(`#google-pay-button`, {
|
||||
buttonType: 'pay',
|
||||
buttonSizeMode: 'fill',
|
||||
});
|
||||
this.loadingGooglePay = false;
|
||||
|
||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
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 retrieve 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());
|
||||
// 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: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
}
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
} finally {
|
||||
// always unlock the checkout once we're finished
|
||||
const verificationToken = await this.squarePaymentService.verifyBuyer(tokenResult.token, tokenResult.details, config.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)}`,
|
||||
config.costUSD,
|
||||
verificationToken.userChallenged
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.squarePaymentService.cleanup();
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
} finally {
|
||||
// always unlock the checkout once we're finished
|
||||
this.isTokenizing--;
|
||||
this.isCheckoutLocked--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -742,7 +646,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
};
|
||||
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
|
||||
const verificationToken = await this.squarePaymentService.verifyBuyer(cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
|
||||
if (!verificationToken || !verificationToken.token) {
|
||||
console.error(`SCA verification failed`);
|
||||
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
||||
|
|
@ -802,107 +706,51 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
/**
|
||||
* CASHAPP
|
||||
*/
|
||||
async requestCashAppPayment(): Promise<void> {
|
||||
async onCashAppTokenized(event: any, config: SquareInitConfig): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
if (this.cashAppPay) {
|
||||
this.cashAppPay.destroy();
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.processing = false;
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
config.costUSD
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.squarePaymentService.cleanup();
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
setTimeout(() => {
|
||||
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')}`, ''));
|
||||
}
|
||||
}, 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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
|
||||
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
|
||||
const paymentRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
}
|
||||
});
|
||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||
});
|
||||
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
this.loadingCashapp = false;
|
||||
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.processing = false;
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
this.accelerationResponse = response;
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
this.cashAppPay.destroy();
|
||||
}
|
||||
this.paymentReceipt.emit(this.accelerationResponse?.receiptUrl);
|
||||
setTimeout(() => {
|
||||
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')}`, ''));
|
||||
}
|
||||
}, 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')}`, ``));
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.squareup.com/docs/sca-overview
|
||||
*/
|
||||
async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
|
||||
const verificationDetails = {
|
||||
amount: amount,
|
||||
currencyCode: 'USD',
|
||||
intent: 'CHARGE',
|
||||
billingContact: {
|
||||
givenName: details.card?.billing?.givenName,
|
||||
familyName: details.card?.billing?.familyName,
|
||||
phone: details.card?.billing?.phone,
|
||||
addressLines: details.card?.billing?.addressLines,
|
||||
city: details.card?.billing?.city,
|
||||
state: details.card?.billing?.state,
|
||||
countryCode: details.card?.billing?.countryCode,
|
||||
},
|
||||
};
|
||||
|
||||
const verificationResults = await payments.verifyBuyer(
|
||||
token,
|
||||
verificationDetails,
|
||||
);
|
||||
return verificationResults;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
679
frontend/src/app/services/square-payment.service.ts
Normal file
679
frontend/src/app/services/square-payment.service.ts
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { log } from '@app/shared/logger.utils';
|
||||
|
||||
type SquarePaymentMethod = 'googlePay' | 'applePay' | 'cashApp' | 'cardOnFile';
|
||||
type SquareCallbackEvent = 'ready' | 'loading' | 'updating' | 'error' | 'cashAppTokenized' | 'googlePayClicked' | 'applePayClicked';
|
||||
type SquareLifecycleStep =
|
||||
| 'start'
|
||||
// setup
|
||||
| 'loading_sdk'
|
||||
| 'setting_up_payments'
|
||||
| 'payments_ready'
|
||||
| 'waiting_to_update'
|
||||
| 'updating_methods'
|
||||
// ready
|
||||
| 'ready'
|
||||
// cleanup
|
||||
| 'waiting_to_cleanup'
|
||||
| 'cleaning_up'
|
||||
type SquareLifecycleStage = 'start' | 'setup' | 'update' | 'updatenow' | 'ready' | 'cleanup';
|
||||
|
||||
export interface SquareInitConfig {
|
||||
availableMethods: {
|
||||
googlePay: boolean;
|
||||
applePay: boolean;
|
||||
cashApp: boolean;
|
||||
};
|
||||
txid: string;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SquarePaymentService {
|
||||
private currentStage: SquareLifecycleStage = 'start';
|
||||
private currentStep: SquareLifecycleStep = 'start';
|
||||
private nextStage: 'setup' | 'cleanup' | 'update' | 'updatenow' | null = null;
|
||||
|
||||
private fresh = true;
|
||||
private updateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private updateResolver: ((value: void | PromiseLike<void>) => void) | null = null;
|
||||
private cleanupTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private cleanupResolver: ((value: void | PromiseLike<void>) => void) | null = null;
|
||||
|
||||
private payments: any = null;
|
||||
private googlePay: any = null;
|
||||
private applePay: any = null;
|
||||
private cashAppPay: any = null;
|
||||
private googlePayRequest: any = null;
|
||||
private applePayRequest: any = null;
|
||||
private cashAppPayRequest: any = null;
|
||||
|
||||
private config: SquareInitConfig | null = null;
|
||||
private newConfig: SquareInitConfig | null = null;
|
||||
|
||||
private callbacks: Partial<Record<SquareCallbackEvent, Function>> = {};
|
||||
private attachedMethods: { googlePay: boolean, applePay: boolean, cashApp: boolean } = { googlePay: false, applePay: false, cashApp: false };
|
||||
|
||||
constructor(private servicesApiService: ServicesApiServices) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.requestStage('setup');
|
||||
}
|
||||
|
||||
async update(config: SquareInitConfig, now = false): Promise<void> {
|
||||
log('[SquarePayment] Updating with config:', config);
|
||||
await this.requestStage(now ? 'updatenow' : 'update', config);
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await this.requestStage('cleanup');
|
||||
}
|
||||
|
||||
private async requestStage(stage: 'setup' | 'update' | 'updatenow' | 'cleanup', config?: SquareInitConfig): Promise<void> {
|
||||
log('[SquarePayment] Requesting stage:', stage);
|
||||
this.newConfig = config;
|
||||
switch (stage) {
|
||||
case 'setup':
|
||||
switch (this.currentStage) {
|
||||
case 'start':
|
||||
await this.executeStage('setup');
|
||||
break;
|
||||
case 'setup':
|
||||
case 'update':
|
||||
case 'updatenow':
|
||||
case 'ready':
|
||||
// already setup or setting up
|
||||
this.nextStage = null;
|
||||
break;
|
||||
case 'cleanup':
|
||||
this.nextStage = 'setup';
|
||||
this.cancelCleanup();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'update':
|
||||
case 'updatenow': {
|
||||
const updateStage = stage as 'update' | 'updatenow';
|
||||
switch (this.currentStage) {
|
||||
case 'start':
|
||||
case 'setup':
|
||||
if (this.currentStep === 'payments_ready') {
|
||||
this.executeStage(updateStage);
|
||||
} else {
|
||||
log('[SquarePayment] Setting next stage to update (currently )...', this.currentStage);
|
||||
this.nextStage = updateStage;
|
||||
}
|
||||
break;
|
||||
case 'update':
|
||||
case 'updatenow':
|
||||
this.nextStage = updateStage;
|
||||
if (this.currentStep === 'waiting_to_update') {
|
||||
log('[SquarePayment] Cancelling queued update...');
|
||||
// haven't actually started updating yet, so we can just cancel the timer, resolve the previous update early, and try again
|
||||
this.cancelUpdate();
|
||||
}
|
||||
break;
|
||||
case 'ready':
|
||||
await this.executeStage(updateStage);
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
case 'cleanup':
|
||||
switch (this.currentStage) {
|
||||
case 'setup':
|
||||
this.nextStage = 'cleanup';
|
||||
break;
|
||||
case 'update':
|
||||
case 'updatenow':
|
||||
this.nextStage = 'cleanup';
|
||||
await this.cancelUpdate();
|
||||
break;
|
||||
case 'ready':
|
||||
await this.executeStage('cleanup');
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeStage(stage: 'setup' | 'update' | 'updatenow' | 'cleanup'): Promise<void> {
|
||||
log('[SquarePayment] Executing stage:', stage);
|
||||
switch (stage) {
|
||||
case 'setup':
|
||||
await this.startSetup();
|
||||
break;
|
||||
case 'update':
|
||||
await this.startUpdate();
|
||||
break;
|
||||
case 'updatenow':
|
||||
await this.startUpdate(true);
|
||||
break;
|
||||
case 'cleanup':
|
||||
await this.startCleanup();
|
||||
break;
|
||||
}
|
||||
if (this.nextStage) {
|
||||
log('[SquarePayment] Moving to next stage:', this.nextStage);
|
||||
const stage = this.nextStage;
|
||||
this.nextStage = null;
|
||||
await this.executeStage(stage);
|
||||
}
|
||||
}
|
||||
|
||||
private async startSetup(): Promise<void> {
|
||||
this.fresh = true;
|
||||
this.emitCallback('loading');
|
||||
this.currentStage = 'setup';
|
||||
this.currentStep = 'loading_sdk';
|
||||
await this.loadSquareSdk();
|
||||
this.currentStep = 'setting_up_payments';
|
||||
await this.setupPayments();
|
||||
this.currentStep = 'payments_ready';
|
||||
}
|
||||
|
||||
private async startUpdate(now = false): Promise<void> {
|
||||
log('[SquarePayment] Starting an update...');
|
||||
this.currentStage = 'update';
|
||||
this.currentStep = 'waiting_to_update';
|
||||
clearTimeout(this.updateTimer);
|
||||
this.emitCallback('loading');
|
||||
this.fresh = false;
|
||||
if (now) {
|
||||
log('[SquarePayment] Running update immediately...');
|
||||
this.currentStep = 'updating_methods';
|
||||
const successfulMethods = await this.updatePaymentMethods();
|
||||
this.emitCallback('ready', successfulMethods);
|
||||
this.currentStep = 'ready';
|
||||
this.currentStage = 'ready';
|
||||
} else {
|
||||
// wait for 2 seconds to debounce frequent updates
|
||||
log('[SquarePayment] Queueing update...');
|
||||
await new Promise<void>((resolve) => {
|
||||
this.updateResolver = resolve;
|
||||
this.updateTimer = setTimeout(async () => {
|
||||
log('[SquarePayment] Running queued update...');
|
||||
this.currentStep = 'updating_methods';
|
||||
const successfulMethods = await this.updatePaymentMethods();
|
||||
this.emitCallback('ready', successfulMethods);
|
||||
this.currentStep = 'ready';
|
||||
this.currentStage = 'ready';
|
||||
this.updateResolver = null;
|
||||
resolve();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
log('[SquarePayment] Update finished');
|
||||
}
|
||||
|
||||
private cancelUpdate(): void {
|
||||
log('[SquarePayment] Cancelling queued update...');
|
||||
const resolve = this.updateResolver;
|
||||
this.updateResolver = null;
|
||||
clearTimeout(this.updateTimer);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async startCleanup(): Promise<void> {
|
||||
log('[SquarePayment] Queueing a cleanup...');
|
||||
this.currentStage = 'cleanup';
|
||||
|
||||
this.detachMethods();
|
||||
|
||||
this.currentStep = 'waiting_to_cleanup';
|
||||
clearTimeout(this.cleanupTimer);
|
||||
await new Promise<void>((resolve) => {
|
||||
this.cleanupResolver = resolve;
|
||||
this.cleanupTimer = setTimeout(async () => {
|
||||
log('[SquarePayment] Running queued cleanup...');
|
||||
this.currentStep = 'cleaning_up';
|
||||
await Promise.all([
|
||||
this.cleanupMethods(),
|
||||
this.cleanupPaymentsAndIframes(),
|
||||
]);
|
||||
this.currentStep = 'start';
|
||||
this.currentStage = 'start';
|
||||
this.cleanupResolver = null;
|
||||
resolve();
|
||||
}, 30000);
|
||||
});
|
||||
log('[SquarePayment] Cleanup finished');
|
||||
}
|
||||
|
||||
private cancelCleanup(): void {
|
||||
log('[SquarePayment] Cancelling queued cleanup...');
|
||||
const resolve = this.cleanupResolver;
|
||||
this.cleanupResolver = null;
|
||||
clearTimeout(this.cleanupTimer);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
registerCallback(event: SquareCallbackEvent, callback: Function): void {
|
||||
this.callbacks[event] = callback;
|
||||
}
|
||||
|
||||
unregisterCallback(event: SquareCallbackEvent): void {
|
||||
delete this.callbacks[event];
|
||||
}
|
||||
|
||||
unregisterAllCallbacks(): void {
|
||||
this.callbacks = {};
|
||||
}
|
||||
|
||||
private emitCallback(event: SquareCallbackEvent, ...args: any[]): void {
|
||||
const callback = this.callbacks[event];
|
||||
if (callback) {
|
||||
callback(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSquareSdk(): Promise<void> {
|
||||
log('[SquarePayment] Loading Square SDK...');
|
||||
|
||||
if (window['Square']) {
|
||||
log('[SquarePayment] Square SDK already loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const isProd = document.location.hostname === 'mempool.space';
|
||||
const scriptUrl = isProd
|
||||
? '/square/v1/square.js'
|
||||
: 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptUrl;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('Failed to load Square SDK'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
await this.waitFor(() => !!window['Square'], 10000);
|
||||
log('[SquarePayment] Square SDK loaded');
|
||||
}
|
||||
|
||||
private async setupPayments(): Promise<void> {
|
||||
log('[SquarePayment] Setting up payments...');
|
||||
|
||||
const setupResult = await firstValueFrom(this.servicesApiService.setupSquare$());
|
||||
if (!setupResult) {
|
||||
throw new Error('Failed to get Square setup configuration');
|
||||
}
|
||||
const { squareAppId, squareLocationId } = setupResult;
|
||||
|
||||
this.payments = window['Square'].payments(squareAppId, squareLocationId);
|
||||
log('[SquarePayment] Payments object created', this.payments);
|
||||
}
|
||||
|
||||
private async updatePaymentMethods(): Promise<string[]> {
|
||||
if (!this.payments || !this.newConfig) {
|
||||
log('[SquarePayment] No payments object or config, skipping method initialization');
|
||||
return;
|
||||
}
|
||||
log('[SquarePayment] Initializing payment methods...');
|
||||
|
||||
const txidChanged = this.newConfig.txid !== this.config?.txid;
|
||||
this.config = this.newConfig;
|
||||
|
||||
const promises: Promise<string | void>[] = [
|
||||
this.config?.availableMethods.googlePay ? this.updateGooglePay(txidChanged).then(() => 'googlepay') : this.removeGooglePay(),
|
||||
this.config?.availableMethods.applePay ? this.updateApplePay(txidChanged).then(() => 'applepay') : this.removeApplePay(),
|
||||
this.config?.availableMethods.cashApp ? this.updateCashApp().then(() => 'cashapp') : this.removeCashApp(),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
return results.map(result => result.status === 'fulfilled' ? result.value : null).filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
private async updateGooglePay(txidChanged: boolean): Promise<void> {
|
||||
log('[SquarePayment] Updating Google Pay...');
|
||||
if (!this.googlePay || txidChanged) {
|
||||
this.removeGooglePay();
|
||||
log('[SquarePayment] Creating Google Pay request...');
|
||||
try {
|
||||
this.googlePayRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: { amount: this.config.costUSD.toFixed(2), label: 'Total' },
|
||||
});
|
||||
this.googlePay = await Promise.race([
|
||||
this.payments.googlePay(this.googlePayRequest, {
|
||||
referenceId: this.buildReferenceId(),
|
||||
}),
|
||||
this.timeout(8000)
|
||||
]);
|
||||
if (!this.googlePay) {
|
||||
throw new Error('Google Pay request timed out');
|
||||
}
|
||||
log('[SquarePayment] Google Pay request created', this.googlePayRequest);
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Google Pay init failed:', e);
|
||||
this.googlePay = null;
|
||||
this.googlePayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log(`[SquarePayment] Updating Google Pay amount`, this.googlePayRequest);
|
||||
try {
|
||||
this.googlePayRequest.update({ total: { amount: this.config.costUSD.toFixed(2), label: 'Total' } });
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Google Pay amount update failed:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!this.attachedMethods.googlePay) {
|
||||
try {
|
||||
const button = await this.waitForElementById('google-pay-button');
|
||||
await this.googlePay.attach(`#google-pay-button`, {
|
||||
buttonType: 'pay',
|
||||
buttonSizeMode: 'fill',
|
||||
});
|
||||
button.addEventListener('click', event => {
|
||||
this.emitCallback('googlePayClicked', event, this.googlePay, this.config);
|
||||
});
|
||||
this.attachedMethods.googlePay = true;
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Google Pay attach failed:', e);
|
||||
this.googlePay = null;
|
||||
this.googlePayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async removeGooglePay(): Promise<void> {
|
||||
if (this.googlePay) {
|
||||
try {
|
||||
if (this.attachedMethods.googlePay) {
|
||||
this.googlePay.detach();
|
||||
this.attachedMethods.googlePay = false;
|
||||
}
|
||||
this.googlePay.destroy();
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Google Pay destroy failed:', e);
|
||||
} finally {
|
||||
this.googlePay = null;
|
||||
this.googlePayRequest = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateApplePay(txidChanged: boolean): Promise<void> {
|
||||
log('[SquarePayment] Updating Apple Pay...');
|
||||
if (!this.applePay || txidChanged) {
|
||||
this.removeApplePay();
|
||||
log('[SquarePayment] Creating Apple Pay request...');
|
||||
try {
|
||||
this.applePayRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: { amount: this.config.costUSD.toFixed(2), label: 'Total' },
|
||||
});
|
||||
this.applePay = await Promise.race([
|
||||
this.payments.applePay(this.applePayRequest),
|
||||
this.timeout(8000)
|
||||
]);
|
||||
if (!this.applePay) {
|
||||
throw new Error('Apple Pay request timed out');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Apple Pay init failed:', e);
|
||||
this.applePay = null;
|
||||
this.applePayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log(`[SquarePayment] Updating Apple Pay amount`);
|
||||
try {
|
||||
this.applePayRequest.update({ total: { amount: this.config.costUSD.toFixed(2), label: 'Total' } });
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Apple Pay amount update failed:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!this.attachedMethods.applePay) {
|
||||
try {
|
||||
const button = await this.waitForElementById('apple-pay-button');
|
||||
button.addEventListener('click', async event => {
|
||||
this.emitCallback('applePayClicked', event, this.applePay, this.config);
|
||||
});
|
||||
this.attachedMethods.applePay = true;
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Apple Pay attach failed:', e);
|
||||
this.applePay = null;
|
||||
this.applePayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async removeApplePay(): Promise<void> {
|
||||
if (this.applePay) {
|
||||
try {
|
||||
if (this.attachedMethods.applePay) {
|
||||
this.attachedMethods.applePay = false;
|
||||
}
|
||||
this.applePay.destroy();
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Apple Pay destroy failed:', e);
|
||||
} finally {
|
||||
this.applePay = null;
|
||||
this.applePayRequest = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCashApp(): Promise<void> {
|
||||
this.removeCashApp();
|
||||
log('[SquarePayment] (Re)Creating Cash App...');
|
||||
try {
|
||||
const redirectHostname = document.location.hostname === 'localhost'
|
||||
? 'http://localhost:4200'
|
||||
: `https://${document.location.hostname}`;
|
||||
|
||||
this.cashAppPayRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: this.config.costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tx/${this.config.txid}`,
|
||||
},
|
||||
});
|
||||
|
||||
this.cashAppPay = await Promise.race([
|
||||
this.payments.cashAppPay(this.cashAppPayRequest, {
|
||||
redirectURL: `${redirectHostname}/tx/${this.config.txid}`,
|
||||
referenceId: this.buildReferenceId(),
|
||||
}),
|
||||
this.timeout(8000)
|
||||
]);
|
||||
if (!this.cashAppPay) {
|
||||
throw new Error('Cash App Pay request timed out');
|
||||
}
|
||||
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
this.emitCallback('cashAppTokenized', event, this.config);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Cash App init failed:', e);
|
||||
this.cashAppPay = null;
|
||||
this.cashAppPayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
if (!this.attachedMethods.cashApp) {
|
||||
try {
|
||||
await this.waitForElementById('cash-app-pay');
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
this.attachedMethods.cashApp = true;
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Cash App Pay attach failed:', e);
|
||||
this.cashAppPay = null;
|
||||
this.cashAppPayRequest = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async removeCashApp(): Promise<void> {
|
||||
log('[SquarePayment] Removing Cash App...');
|
||||
if (this.cashAppPay) {
|
||||
try {
|
||||
if (this.attachedMethods.cashApp) {
|
||||
this.cashAppPay.detach();
|
||||
this.attachedMethods.cashApp = false;
|
||||
}
|
||||
this.cashAppPay.destroy();
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Cash App Pay destroy failed:', e);
|
||||
} finally {
|
||||
this.cashAppPay = null;
|
||||
this.cashAppPayRequest = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private detachMethods(): void {
|
||||
if (this.attachedMethods.googlePay) {
|
||||
this.googlePay.detach();
|
||||
this.attachedMethods.googlePay = false;
|
||||
}
|
||||
this.attachedMethods.applePay = false;
|
||||
if (this.attachedMethods.cashApp) {
|
||||
this.cashAppPay.detach();
|
||||
this.attachedMethods.cashApp = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupMethods(): Promise<void> {
|
||||
await this.removeGooglePay();
|
||||
await this.removeApplePay();
|
||||
await this.removeCashApp();
|
||||
}
|
||||
|
||||
private cleanupPaymentsAndIframes(): void {
|
||||
log('[SquarePayment] Cleaning up payments and iframes...');
|
||||
|
||||
this.payments = null;
|
||||
|
||||
// remove Square from window
|
||||
delete window['Square'];
|
||||
|
||||
// clean up main square script
|
||||
document.querySelectorAll('head > script').forEach(script => {
|
||||
const src = script.getAttribute('src') || '';
|
||||
if (
|
||||
src.includes('square') ||
|
||||
src.includes('squareup') ||
|
||||
src.includes('squarecdn') ||
|
||||
src.includes('cash.app') ||
|
||||
src.includes('pay.google.com')
|
||||
) {
|
||||
log('[SquarePayment] Removing script', src);
|
||||
script.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// clean up iframes
|
||||
document.querySelectorAll('body > iframe').forEach(iframe => {
|
||||
const src = iframe.getAttribute('src') || '';
|
||||
if (
|
||||
src.includes('square') ||
|
||||
src.includes('squareup') ||
|
||||
src.includes('squarecdn') ||
|
||||
src.includes('cash.app') ||
|
||||
src.includes('pay.google.com')
|
||||
) {
|
||||
log('[SquarePayment] Removing iframe', src);
|
||||
iframe.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// clean up payment method scripts
|
||||
document.querySelectorAll('script[id^="square-payments-"]').forEach(script => {
|
||||
log('[SquarePayment] Removing payment method script', script.getAttribute('id'));
|
||||
script.remove();
|
||||
});
|
||||
}
|
||||
|
||||
public async verifyBuyer(token: string, details: any, amount: string): Promise<{ token: string; userChallenged: boolean } | null> {
|
||||
try {
|
||||
const verificationDetails = {
|
||||
amount,
|
||||
currencyCode: 'USD',
|
||||
intent: 'CHARGE',
|
||||
billingContact: {
|
||||
givenName: details.card?.billing?.givenName,
|
||||
familyName: details.card?.billing?.familyName,
|
||||
phone: details.card?.billing?.phone,
|
||||
addressLines: details.card?.billing?.addressLines,
|
||||
city: details.card?.billing?.city,
|
||||
state: details.card?.billing?.state,
|
||||
countryCode: details.card?.billing?.countryCode,
|
||||
},
|
||||
};
|
||||
|
||||
return await this.payments.verifyBuyer(token, verificationDetails);
|
||||
} catch (e) {
|
||||
console.error('[SquarePayment] Buyer verification failed:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private buildReferenceId(): string {
|
||||
return `accelerator-${this.config?.txid?.substring(0, 15)}-${Math.round(Date.now() / 1000)}`;
|
||||
}
|
||||
|
||||
private async timeout(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private async waitFor(condition: () => boolean, timeout: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const check = () => {
|
||||
if (condition()) {
|
||||
resolve();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
reject(new Error('Timeout waiting for condition'));
|
||||
} else {
|
||||
setTimeout(check, 100);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
private waitForElementById(id: string, timeout = 10000): Promise<HTMLElement> {
|
||||
const selector = `#${CSS.escape(id)}`;
|
||||
const existing = document.querySelector<HTMLElement>(selector);
|
||||
if (existing) {
|
||||
return Promise.resolve(existing);
|
||||
}
|
||||
return new Promise<HTMLElement>((resolve, reject) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector<HTMLElement>(selector);
|
||||
if (el) {
|
||||
observer.disconnect();
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
reject(new Error(`timeout waiting for ${selector}`));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
6
frontend/src/app/shared/logger.utils.ts
Normal file
6
frontend/src/app/shared/logger.utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
window['DEV_MODE'] = localStorage.getItem('dev_mode') === 'true';
|
||||
export function log(...msgs: any[]): void {
|
||||
if (window['DEV_MODE']) {
|
||||
console.log(...msgs);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue