[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,103 +1,147 @@
<div class="container card" style="padding: 20px; background: var(--bg)"> <div class="container card" style="padding: 20px; background: var(--bg)">
@if (!showCheckoutPage) { @if (error) {
<!-- Show A/B CTAs --> <app-mempool-error [error]="error"></app-mempool-error>
<div class="row mb-3"> } @else {
<div class="col-sm">
<h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
</div>
</div>
<form> @if (step === 'completed') {
<div class="row"> <div class="row text-center mt-3">
<div class="col-sm">
<div class="form-group form-check">
<input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accelerate">
<span class="font-weight-bold">Accelerate</span>
<span style="color: rgb(186, 186, 186)">Settlement expected in ~1 hour or less<br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats|sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group form-check">
<input type="radio" class="form-check-input" id="wait" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="wait">
<span class="font-weight-bold">Wait</span>
<span style="color: rgb(186, 186, 186)">Settlement expected to occur <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></span>
</label>
</div>
</div>
</div>
<div class="row mt-2" [style]="choosenOption === 'wait' ? 'opacity: 0.25; pointer-events: none' : ''">
<div class="col-sm d-flex flex-row justify-content-center">
<button type="button" class="mt-1 btn btn-light rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="enableCheckoutPage()">
<img src="/resources/mempool-accelerator-sparkles-compressed.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Accelerate</span>
</button>
</div>
</div>
</form>
}
@else {
<!-- Show checkout page -->
<div class="row mb-3 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
</div>
</div>
<div class="row text-center">
<div class="col-sm">
<div class="form-group w-100">
Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + txid" target="_blank">{{ txid.substr(0, 10) }}..{{ txid.substr(-10) }}</a>
</div>
</div>
</div>
@if (!loadingCashapp) {
<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">
<span><u><strong>Total additional cost</strong></u><br> <div display="d-flex flex-row justify-content-center">
<span style="font-size: 16px" class="d-block mt-2"> <div class="alert alert-success">Transaction is now being accelerated!</div>
Pay </div>
<strong><app-fiat [value]="cost"></app-fiat></strong>
with
</span>
</span>
</div> </div>
</div> </div>
</div> </div>
} }
<div class="row text-center mt-1"> @else if (step === 'cta') {
<div class="col-sm"> <!-- Show A/B CTAs -->
<div class="form-group w-100"> <div class="row mb-3">
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''" (click)="submitCashappPay()"></div> <div class="col-sm">
@if (loadingCashapp) { <h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
<div class="spinner-border text-light" style="width: 25px; height: 25px"></div>
}
</div> </div>
</div> </div>
</div>
<hr> <form>
<div class="row mt-2 text-center"> <div class="row">
<div class="col-sm d-flex flex-column"> <div class="col-sm">
<small>Changed your mind?</small> <div class="form-group form-check">
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="restart()">Cancel</button> <input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accelerate">
<span class="font-weight-bold">Accelerate</span>
<span style="color: rgb(186, 186, 186)">Settlement expected in ~1 hour or less<br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats|sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group form-check">
<input type="radio" class="form-check-input" id="wait" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="wait">
<span class="font-weight-bold">Wait</span>
<span style="color: rgb(186, 186, 186)">Settlement expected to occur <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></span>
</label>
</div>
</div>
</div>
<div class="row mt-2" [style]="choosenOption === 'wait' ? 'opacity: 0.25; pointer-events: none' : ''">
<div class="col-sm d-flex flex-row justify-content-center">
<button type="button" class="mt-1 btn btn-light rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="enableCheckoutPage()">
<img src="/resources/mempool-accelerator-sparkles-compressed.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Accelerate</span>
</button>
</div>
</div>
</form>
}
@else if (step === 'checkout') {
<!-- Show checkout page -->
<div class="row mb-3 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
</div>
</div> </div>
</div>
}
<div class="row text-center">
<div class="col-sm">
<div class="form-group w-100">
Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + txid" target="_blank">{{ txid.substr(0, 10) }}..{{ txid.substr(-10) }}</a>
</div>
</div>
</div>
@if (!loadingCashapp) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<span><u><strong>Total additional cost</strong></u><br>
<span style="font-size: 16px" class="d-block mt-2">
Pay
<strong><app-fiat [value]="cost"></app-fiat></strong>
with
</span>
</span>
</div>
</div>
</div>
}
<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) {
<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>
<hr>
<div class="row mt-2 text-center">
<div class="col-sm d-flex flex-column">
<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(); closeModal()">Close</button>
</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.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
this.estimate();
});
} }
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
if (this.step === 'cta') {
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,12 +181,16 @@ 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) {
const { tokenResult, error } = event.detail; const { tokenResult, error } = event.detail;
@ -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[]> {