From 58e6a785795bae3196fd99ab7371199ff88dc358 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 19 Jan 2025 17:56:19 +0900 Subject: [PATCH] [accelerator] add support for card on file acceleration --- .../accelerate-checkout.component.html | 22 ++- .../accelerate-checkout.component.ts | 133 +++++++++++++++++- .../src/app/services/services-api.service.ts | 4 + frontend/src/app/shared/shared.module.ts | 3 +- 4 files changed, 153 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 150da04da..e90a5cc21 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -389,13 +389,13 @@ } - @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {

OR

} } - @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {

Pay  with

@if (canPayWithCashapp) { @@ -413,6 +413,13 @@
} + @if (canPayWithCardOnFile) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { } +
+ + {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }} +
+ } } @@ -435,7 +442,7 @@ - } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') { + } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
@@ -451,7 +458,7 @@
- @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) { + @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
@@ -476,8 +483,13 @@
} @else if (step === 'googlepay') {
+ } @else if (step === 'cardonfile') { +
+ + {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }} +
} - @if (loadingCashapp || loadingApplePay || loadingGooglePay) { + @if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
Loading payment method...
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 34a98b8dc..e516fe321 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service'; import { ApiService } from '@app/services/api.service'; 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 = { hasAccess: boolean; @@ -26,7 +26,7 @@ export type AccelerationEstimate = { mempoolBaseFee: number; vsizeFee: number; pools: number[]; - availablePaymentMethods: Record; + availablePaymentMethods: Record; unavailable?: boolean; options: { // recommended bid options fee: number; // recommended userBid in sats @@ -49,7 +49,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' | 'processing' | 'paid' | 'success'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success'; @Component({ selector: 'app-accelerate-checkout', @@ -65,6 +65,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() cashappEnabled: boolean = true; @Input() applePayEnabled: boolean = false; @Input() googlePayEnabled: boolean = true; + @Input() cardOnFileEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -117,6 +118,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { loadingCashapp = false; loadingApplePay = false; loadingGooglePay = false; + loadingCardOnFile = false; payments: any; cashAppPay: any; applePay: any; @@ -234,6 +236,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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(() => { @@ -454,6 +460,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { await this.requestApplePayPayment(); } else if (this._step === 'googlepay') { await this.requestGooglePayPayment(); + } else if (this._step === 'cardonfile') { + this.loadingCardOnFile = false; } }, error: () => { @@ -710,6 +718,109 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ); } + /** + * Card On File + */ + async requestCardOnFilePayment(): Promise { + 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 */ @@ -955,6 +1066,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { 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 { if (!this.hasAccessToBalanceMode) { return false; diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 5e882cd02..59dc92358 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -146,6 +146,10 @@ export class ServicesApiServices { return this.httpClient.post(`${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(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 1d4f5fd99..e78e74a9c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, - faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot } from '@fortawesome/free-solid-svg-icons'; + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faCreditCard } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -459,5 +459,6 @@ export class SharedModule { library.addIcons(faCalendarCheck); library.addIcons(faMoneyBillTrendUp); library.addIcons(faRobot); + library.addIcons(faCreditCard); } }