Merge pull request #5353 from mempool/nymkappa/apple-pay
[accelerator] add support for acceleration with apple pay
This commit is contained in:
@@ -389,18 +389,22 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
||||
<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) {
|
||||
<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>
|
||||
<img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
</div>
|
||||
}
|
||||
<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) { <hr class="w-25 mt-2 mb-2"> }
|
||||
<img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (click)="moveToStep('applepay')">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -421,9 +425,9 @@
|
||||
<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') {
|
||||
} @else if (step === 'cashapp' || step === 'applepay') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center">
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
<h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1>
|
||||
</div>
|
||||
@@ -437,7 +441,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!loadingCashapp) {
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@@ -456,8 +460,12 @@
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
@if (loadingCashapp) {
|
||||
@if (step === 'applepay') {
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-white" [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]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
}
|
||||
@if (loadingCashapp || loadingApplePay) {
|
||||
<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>
|
||||
@@ -549,7 +557,7 @@
|
||||
<button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
<ng-template id="accelerate-to" #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
|
||||
<ng-template #accelerateButton>
|
||||
<div class="position-relative">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user