Merge pull request #5732 from mempool/nymkappa/accelerator-card-on-file
[accelerator] add support for card on file acceleration
This commit is contained in:
commit
c4a7a2e781
@ -397,13 +397,13 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
<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>
|
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
<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>
|
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||||
@if (canPayWithCashapp) {
|
@if (canPayWithCashapp) {
|
||||||
@ -421,6 +421,13 @@
|
|||||||
<img src="/resources/google-pay.png" height=37>
|
<img src="/resources/google-pay.png" height=37>
|
||||||
</div>
|
</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')">
|
||||||
|
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
|
||||||
|
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -443,7 +450,7 @@
|
|||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
|
||||||
<!-- Show checkout page -->
|
<!-- Show checkout page -->
|
||||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||||
<div class="col-sm" id="confirm-payment-title">
|
<div class="col-sm" id="confirm-payment-title">
|
||||||
@ -459,7 +466,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
|
||||||
<div class="row text-center mt-1">
|
<div class="row text-center mt-1">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group w-100">
|
<div class="form-group w-100">
|
||||||
@ -484,8 +491,13 @@
|
|||||||
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
<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') {
|
} @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>
|
<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="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
|
||||||
|
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
|
||||||
|
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
|
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
|
||||||
<div display="d-flex flex-row justify-content-center">
|
<div display="d-flex flex-row justify-content-center">
|
||||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
<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-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||||
|
@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
|
|||||||
import { ApiService } from '@app/services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { isDevMode } from '@angular/core';
|
import { isDevMode } from '@angular/core';
|
||||||
|
|
||||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
|
||||||
|
|
||||||
export type AccelerationEstimate = {
|
export type AccelerationEstimate = {
|
||||||
hasAccess: boolean;
|
hasAccess: boolean;
|
||||||
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
|
|||||||
mempoolBaseFee: number;
|
mempoolBaseFee: number;
|
||||||
vsizeFee: number;
|
vsizeFee: number;
|
||||||
pools: number[];
|
pools: number[];
|
||||||
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
|
||||||
unavailable?: boolean;
|
unavailable?: boolean;
|
||||||
options: { // recommended bid options
|
options: { // recommended bid options
|
||||||
fee: number; // recommended userBid in sats
|
fee: number; // recommended userBid in sats
|
||||||
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
|
|||||||
export const DEFAULT_BID_RATIO = 2;
|
export const DEFAULT_BID_RATIO = 2;
|
||||||
export const MAX_BID_RATIO = 4;
|
export const MAX_BID_RATIO = 4;
|
||||||
|
|
||||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
|
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accelerate-checkout',
|
selector: 'app-accelerate-checkout',
|
||||||
@ -65,6 +65,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Input() cashappEnabled: boolean = true;
|
@Input() cashappEnabled: boolean = true;
|
||||||
@Input() applePayEnabled: boolean = false;
|
@Input() applePayEnabled: boolean = false;
|
||||||
@Input() googlePayEnabled: boolean = true;
|
@Input() googlePayEnabled: boolean = true;
|
||||||
|
@Input() cardOnFileEnabled: boolean = true;
|
||||||
@Input() advancedEnabled: boolean = false;
|
@Input() advancedEnabled: boolean = false;
|
||||||
@Input() forceMobile: boolean = false;
|
@Input() forceMobile: boolean = false;
|
||||||
@Input() showDetails: boolean = false;
|
@Input() showDetails: boolean = false;
|
||||||
@ -117,6 +118,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
loadingCashapp = false;
|
loadingCashapp = false;
|
||||||
loadingApplePay = false;
|
loadingApplePay = false;
|
||||||
loadingGooglePay = false;
|
loadingGooglePay = false;
|
||||||
|
loadingCardOnFile = false;
|
||||||
payments: any;
|
payments: any;
|
||||||
cashAppPay: any;
|
cashAppPay: any;
|
||||||
applePay: any;
|
applePay: any;
|
||||||
@ -234,6 +236,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.loadingGooglePay = true;
|
this.loadingGooglePay = true;
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
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') {
|
} else if (this._step === 'paid') {
|
||||||
this.timePaid = Date.now();
|
this.timePaid = Date.now();
|
||||||
this.timeoutTimer = setTimeout(() => {
|
this.timeoutTimer = setTimeout(() => {
|
||||||
@ -454,6 +460,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
await this.requestApplePayPayment();
|
await this.requestApplePayPayment();
|
||||||
} else if (this._step === 'googlepay') {
|
} else if (this._step === 'googlepay') {
|
||||||
await this.requestGooglePayPayment();
|
await this.requestGooglePayPayment();
|
||||||
|
} else if (this._step === 'cardonfile') {
|
||||||
|
this.loadingCardOnFile = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@ -710,6 +718,109 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card On File
|
||||||
|
*/
|
||||||
|
async requestCardOnFilePayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.conversionsSubscription) {
|
||||||
|
this.conversionsSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
|
async (conversions) => {
|
||||||
|
this.conversions = conversions;
|
||||||
|
|
||||||
|
const costUSD = this.cost / 100_000_000 * conversions.USD;
|
||||||
|
if (this.isCheckoutLocked > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
|
||||||
|
if (!cardOnFile?.card) {
|
||||||
|
this.accelerateError = 'card_on_file_not_found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadingCardOnFile = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isCheckoutLocked += 2;
|
||||||
|
this.isTokenizing += 2;
|
||||||
|
|
||||||
|
const nameParts = cardOnFile.card.name.split(' ');
|
||||||
|
const assumedGivenName = nameParts[0];
|
||||||
|
const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
|
||||||
|
const verificationDetails = {
|
||||||
|
card: {
|
||||||
|
billing: {
|
||||||
|
givenName: assumedGivenName,
|
||||||
|
familyName: assumedFamilyName,
|
||||||
|
addressLines: [cardOnFile.card.billing.addressLine1],
|
||||||
|
city: cardOnFile.card.billing.locality,
|
||||||
|
state: cardOnFile.card.billing.administrativeDistrictLevel1,
|
||||||
|
countyCode: cardOnFile.card.billing.country,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
|
||||||
|
if (!verificationToken || !verificationToken.token) {
|
||||||
|
console.error(`SCA verification failed`);
|
||||||
|
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.servicesApiService.accelerateWithCardOnFile$(
|
||||||
|
this.tx.txid,
|
||||||
|
cardOnFile.card.card_id,
|
||||||
|
verificationToken.token,
|
||||||
|
`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');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.isTokenizing--;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = e.message;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// always unlock the checkout once we're finished
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.isTokenizing--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CASHAPP
|
* CASHAPP
|
||||||
*/
|
*/
|
||||||
@ -955,6 +1066,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canPayWithCardOnFile(): boolean {
|
||||||
|
if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
|
||||||
|
if (paymentMethod) {
|
||||||
|
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||||
|
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
get canPayWithBalance(): boolean {
|
get canPayWithBalance(): boolean {
|
||||||
if (!this.hasAccessToBalanceMode) {
|
if (!this.hasAccessToBalanceMode) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -146,6 +146,10 @@ export class ServicesApiServices {
|
|||||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
|
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
|
||||||
|
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
|
||||||
|
}
|
||||||
|
|
||||||
getAccelerations$(): Observable<Acceleration[]> {
|
getAccelerations$(): Observable<Acceleration[]> {
|
||||||
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
|
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
|
|||||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
||||||
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||||
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
|
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
|
||||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons';
|
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { MenuComponent } from '@components/menu/menu.component';
|
import { MenuComponent } from '@components/menu/menu.component';
|
||||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
||||||
@ -463,5 +463,6 @@ export class SharedModule {
|
|||||||
library.addIcons(faMoneyBillTrendUp);
|
library.addIcons(faMoneyBillTrendUp);
|
||||||
library.addIcons(faRobot);
|
library.addIcons(faRobot);
|
||||||
library.addIcons(faShareNodes);
|
library.addIcons(faShareNodes);
|
||||||
|
library.addIcons(faCreditCard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user