[accelerator] polish UI prepaid accel

This commit is contained in:
nymkappa 2024-04-13 23:07:19 +09:00
parent f601bbc499
commit fb086b5ad5
No known key found for this signature in database
GPG Key ID: 92358FC85D9645DE
6 changed files with 192 additions and 127 deletions

View File

@ -1,6 +1,22 @@
<div class="container card" style="padding: 20px; background: var(--bg)"> <div class="container card" style="padding: 20px; background: var(--bg)">
@if (!showCheckoutPage) { @if (error) {
<app-mempool-error [error]="error"></app-mempool-error>
} @else {
@if (step === 'completed') {
<div class="row text-center mt-3">
<div class="col-sm">
<div class="form-group w-100">
<div display="d-flex flex-row justify-content-center">
<div class="alert alert-success">Transaction is now being accelerated!</div>
</div>
</div>
</div>
</div>
}
@else if (step === 'cta') {
<!-- Show A/B CTAs --> <!-- Show A/B CTAs -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm"> <div class="col-sm">
@ -48,7 +64,7 @@
</form> </form>
} }
@else { @else if (step === 'checkout') {
<!-- Show checkout page --> <!-- Show checkout page -->
<div class="row mb-3 text-center"> <div class="row mb-3 text-center">
<div class="col-sm"> <div class="col-sm">
@ -83,9 +99,12 @@
<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">
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''" (click)="submitCashappPay()"></div> <div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
@if (loadingCashapp) { @if (loadingCashapp) {
<div class="spinner-border text-light" style="width: 25px; height: 25px"></div> <div display="d-flex flex-row justify-content-center">
<span>Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} }
</div> </div>
</div> </div>
@ -95,9 +114,34 @@
<div class="row mt-2 text-center"> <div class="row mt-2 text-center">
<div class="col-sm d-flex flex-column"> <div class="col-sm d-flex flex-column">
<small>Changed your mind?</small> <small>Changed your mind?</small>
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="restart()">Cancel</button> <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="restart(); closeModal()">Close</button>
</div> </div>
</div> </div>
} }
@else if (step === 'processing') {
<div class="row mb-3 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
</div>
</div>
<!-- Processing payment -->
<div id="cash-app-pay" class="d-inline-block" [style]="'opacity: 0; width: 0px; height: 0px; pointer-events: none;'"></div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<div display="d-flex flex-row justify-content-center">
<span>We are processing your payment...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</div>
}
<span class="close-button" (click)="closeModal()"></span>
}
</div> </div>

View File

@ -1,9 +1,10 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef } from '@angular/core';
import { Subscription, tap, of, catchError } from 'rxjs'; import { Subscription, tap, of, catchError } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { nextRoundNumber } from '../../shared/common.utils'; import { nextRoundNumber } from '../../shared/common.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
@Component({ @Component({
selector: 'app-accelerate-checkout', selector: 'app-accelerate-checkout',
@ -13,47 +14,56 @@ import { StateService } from '../../services/state.service';
export class AccelerateCheckout implements OnInit, OnDestroy { export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number = Date.now() + 123456789; @Input() eta: number = Date.now() + 123456789;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad'; @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Output() close = new EventEmitter<null>();
calculating = true; calculating = true;
choosenOption: 'wait' | 'accelerate' = 'wait'; choosenOption: 'wait' | 'accelerate' = 'wait';
showCheckoutPage = false;
error = ''; error = '';
// accelerator stuff // accelerator stuff
square: { appId: string, locationId: string}; square: { appId: string, locationId: string};
accelerationUUID: string; accelerationUUID: string;
estimateSubscription: Subscription; estimateSubscription: Subscription;
maxBidBoost: number; // sats
cost: number; // sats cost: number; // sats
// square // square
loadingCashapp = false;
cashappSubmit: any; cashappSubmit: any;
payments: any; payments: any;
cashAppPay: any; cashAppPay: any;
cashAppSubscription: Subscription; cashAppSubscription: Subscription;
conversionsSubscription: Subscription; conversionsSubscription: Subscription;
loadingCashapp = true; step: 'cta' | 'checkout' | 'processing' | 'completed' = 'completed';
processingPayment = true;
constructor( constructor(
private websocketService: WebsocketService, private websocketService: WebsocketService,
private servicesApiService: ServicesApiServices, private servicesApiService: ServicesApiServices,
private stateService: StateService private stateService: StateService,
) {} private audioService: AudioService,
private cd: ChangeDetectorRef
) {
this.accelerationUUID = window.crypto.randomUUID();
}
ngOnInit() { ngOnInit() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.processingPayment = true; this.insertSquare();
window.scrollTo(0, 0); this.setupSquare();
} else { this.step = 'processing';
}
this.servicesApiService.setupSquare$().subscribe(ids => { this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = { this.square = {
appId: ids.squareAppId, appId: ids.squareAppId,
locationId: ids.squareLocationId locationId: ids.squareLocationId
}; };
if (this.step === 'cta') {
this.estimate(); this.estimate();
});
} }
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -82,9 +92,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return; return;
} }
// Make min extra fee at least 50% of the current tx fee // Make min extra fee at least 50% of the current tx fee
const minExtraCost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee)); const minExtraBoost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee));
const DEFAULT_BID_RATIO = 2; const DEFAULT_BID_RATIO = 2;
this.cost = minExtraCost * DEFAULT_BID_RATIO + estimation.mempoolBaseFee + estimation.vsizeFee; this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
this.cost = this.maxBidBoost * DEFAULT_BID_RATIO + estimation.mempoolBaseFee + estimation.vsizeFee;
} }
}), }),
@ -143,8 +154,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
} }
async requestCashAppPayment() { async requestCashAppPayment() {
this.loadingCashapp = true;
if (this.cashAppSubscription) { if (this.cashAppSubscription) {
this.cashAppSubscription.unsubscribe(); this.cashAppSubscription.unsubscribe();
} }
@ -155,11 +164,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
if (this.cashAppPay) { if (this.cashAppPay) {
this.cashAppPay.destroy(); await this.cashAppPay.destroy();
} }
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`; const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.cost / 100_000_000 * conversions.USD; const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
const paymentRequest = this.payments.paymentRequest({ const paymentRequest = this.payments.paymentRequest({
countryCode: 'US', countryCode: 'US',
currencyCode: 'USD', currencyCode: 'USD',
@ -172,11 +181,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
button: { shape: 'semiround', size: 'small', theme: 'light'} button: { shape: 'semiround', size: 'small', theme: 'light'}
}); });
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tracker/${this.txid}?acceleration=false`, redirectURL: `${redirectHostname}/tracker/${this.txid}`,
referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
button: { shape: 'semiround', size: 'small', theme: 'light'} button: { shape: 'semiround', size: 'small', theme: 'light'}
}); });
this.cashappSubmit = await this.cashAppPay.CashAppPayInstance.render('#cash-app-pay', { button: { theme: 'light', size: 'small', shape: 'semiround' }, manage: false });
if (this.step === 'checkout') {
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
}
this.loadingCashapp = false;
const that = this; const that = this;
this.cashAppPay.addEventListener('ontokenization', function (event) { this.cashAppPay.addEventListener('ontokenization', function (event) {
@ -186,14 +199,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} else if (tokenResult.status === 'OK') { } else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$( that.servicesApiService.accelerateWithCashApp$(
that.txid, that.txid,
that.cost,
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
that.accelerationUUID that.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
that.estimateSubscription.unsubscribe(); that.audioService.playSound('ascend-chime-cartoon');
that.step = 'completed';
setTimeout(() => {
that.closeModal();
}, 10000);
}, },
error: (response) => { error: (response) => {
if (response.status === 403 && response.error === 'not_available') { if (response.status === 403 && response.error === 'not_available') {
@ -205,33 +221,34 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}); });
} }
}); });
this.loadingCashapp = false;
} }
); );
} }
submitCashappPay(): void {
if (this.cashappSubmit) {
this.cashappSubmit?.begin();
this.processingPayment = true;
}
}
/** /**
* UI events * UI events
*/ */
enableCheckoutPage() { enableCheckoutPage() {
this.showCheckoutPage = true; this.step = 'checkout';
this.loadingCashapp = true;
this.insertSquare(); this.insertSquare();
this.setupSquare(); this.setupSquare();
} }
selectedOptionChanged(event) { selectedOptionChanged(event) {
this.choosenOption = event.target.id; this.choosenOption = event.target.id;
if (this.choosenOption === 'wait') {
this.restart();
this.closeModal();
}
} }
restart() { restart() {
this.showCheckoutPage = false this.step = 'cta';
this.choosenOption = 'wait'; this.choosenOption = 'wait';
} }
closeModal(): void { closeModal(): void {
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
this.close.emit(); this.close.emit();
} }
} }

View File

@ -362,7 +362,6 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$( that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$(
that.tx.txid, that.tx.txid,
that.userBid,
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,

View File

@ -48,7 +48,7 @@
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
} }
@if (isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { @if (isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
<a [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> <a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
} }
</span> </span>
</div> </div>

View File

@ -146,6 +146,11 @@ export class TrackerComponent implements OnInit, OnDestroy {
if (this.acceleratorAvailable && this.stateService.ref === 'https://cash.app/') { if (this.acceleratorAvailable && this.stateService.ref === 'https://cash.app/') {
this.paymentType = 'cashapp'; this.paymentType = 'cashapp';
} }
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) {
this.showAccelerationSummary = true;
}
this.showAccelerationSummary = true;
this.enterpriseService.page(); this.enterpriseService.page();

View File

@ -132,8 +132,8 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
} }
accelerateWithCashApp$(txInput: string, userBid: number, token: string, cashtag: string, referenceId: string, accelerationUUID: string) { accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, userBid: userBid, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
} }
getAccelerations$(): Observable<Acceleration[]> { getAccelerations$(): Observable<Acceleration[]> {