[accelerator] add support for card on file acceleration
This commit is contained in:
		
							parent
							
								
									703241acf0
								
							
						
					
					
						commit
						58e6a78579
					
				| @ -389,13 +389,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) { | ||||||
| @ -413,6 +413,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> | ||||||
| @ -435,7 +442,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"> | ||||||
| @ -451,7 +458,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"> | ||||||
| @ -476,8 +483,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 } from '@fortawesome/free-solid-svg-icons'; |   faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, 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'; | ||||||
| @ -459,5 +459,6 @@ export class SharedModule { | |||||||
|     library.addIcons(faCalendarCheck); |     library.addIcons(faCalendarCheck); | ||||||
|     library.addIcons(faMoneyBillTrendUp); |     library.addIcons(faMoneyBillTrendUp); | ||||||
|     library.addIcons(faRobot); |     library.addIcons(faRobot); | ||||||
|  |     library.addIcons(faCreditCard); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user