[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)">
@if (!showCheckoutPage) {
<!-- Show A/B CTAs -->
<div class="row mb-3">
<div class="col-sm">
<h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
</div>
</div>
@if (error) {
<app-mempool-error [error]="error"></app-mempool-error>
} @else {
<form>
<div class="row">
<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">
@if (step === 'completed') {
<div class="row text-center mt-3">
<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 display="d-flex flex-row justify-content-center">
<div class="alert alert-success">Transaction is now being accelerated!</div>
</div>
</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;' : ''" (click)="submitCashappPay()"></div>
@if (loadingCashapp) {
<div class="spinner-border text-light" style="width: 25px; height: 25px"></div>
}
@else if (step === 'cta') {
<!-- Show A/B CTAs -->
<div class="row mb-3">
<div class="col-sm">
<h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
</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()">Cancel</button>
<form>
<div class="row">
<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 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 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>

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 { WebsocketService } from '../../services/websocket.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { nextRoundNumber } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
@Component({
selector: 'app-accelerate-checkout',
@ -13,47 +14,56 @@ import { StateService } from '../../services/state.service';
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number = Date.now() + 123456789;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Output() close = new EventEmitter<null>();
calculating = true;
choosenOption: 'wait' | 'accelerate' = 'wait';
showCheckoutPage = false;
error = '';
// accelerator stuff
square: { appId: string, locationId: string};
accelerationUUID: string;
estimateSubscription: Subscription;
maxBidBoost: number; // sats
cost: number; // sats
// square
loadingCashapp = false;
cashappSubmit: any;
payments: any;
cashAppPay: any;
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
loadingCashapp = true;
processingPayment = true;
step: 'cta' | 'checkout' | 'processing' | 'completed' = 'completed';
constructor(
private websocketService: WebsocketService,
private servicesApiService: ServicesApiServices,
private stateService: StateService
) {}
private stateService: StateService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) {
this.accelerationUUID = window.crypto.randomUUID();
}
ngOnInit() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.processingPayment = true;
window.scrollTo(0, 0);
} else {
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
this.estimate();
});
this.insertSquare();
this.setupSquare();
this.step = 'processing';
}
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
if (this.step === 'cta') {
this.estimate();
}
});
}
ngOnDestroy() {
@ -82,9 +92,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return;
}
// 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;
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() {
this.loadingCashapp = true;
if (this.cashAppSubscription) {
this.cashAppSubscription.unsubscribe();
}
@ -155,11 +164,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
if (this.cashAppPay) {
this.cashAppPay.destroy();
await this.cashAppPay.destroy();
}
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({
countryCode: 'US',
currencyCode: 'USD',
@ -172,12 +181,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
button: { shape: 'semiround', size: 'small', theme: 'light'}
});
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)}`,
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;
this.cashAppPay.addEventListener('ontokenization', function (event) {
const { tokenResult, error } = event.detail;
@ -186,14 +199,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$(
that.txid,
that.cost,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
that.accelerationUUID
).subscribe({
next: () => {
that.estimateSubscription.unsubscribe();
that.audioService.playSound('ascend-chime-cartoon');
that.step = 'completed';
setTimeout(() => {
that.closeModal();
}, 10000);
},
error: (response) => {
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
*/
enableCheckoutPage() {
this.showCheckoutPage = true;
this.step = 'checkout';
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
}
selectedOptionChanged(event) {
this.choosenOption = event.target.id;
if (this.choosenOption === 'wait') {
this.restart();
this.closeModal();
}
}
restart() {
this.showCheckoutPage = false
this.step = 'cta';
this.choosenOption = 'wait';
}
closeModal(): void {
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
this.close.emit();
}
}

View File

@ -362,7 +362,6 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$(
that.tx.txid,
that.userBid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
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>
}
@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>
</div>

View File

@ -146,6 +146,11 @@ export class TrackerComponent implements OnInit, OnDestroy {
if (this.acceleratorAvailable && this.stateService.ref === 'https://cash.app/') {
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();

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 });
}
accelerateWithCashApp$(txInput: string, userBid: number, 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 });
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
}
getAccelerations$(): Observable<Acceleration[]> {