-
-
-
-
-
-
-
-
-
-
-
✖
-
\ No newline at end of file
+
+
+
+ Changed your mind?
+
+
+
+ }
+
+ @else if (step === 'processing') {
+
+
+
Confirm your payment
+
+
+
+
+
+
+
+ }
+
+
✖
+ }
+
+
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
index 617147221..315bdbbd2 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
@@ -2,4 +2,8 @@
position: absolute;
top: 0.5em;
right: 0.5em;
-}
\ No newline at end of file
+}
+
+.estimating {
+ color: var(--green)
+}
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 2de1aa34b..622256461 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
@@ -1,6 +1,10 @@
-import { Component, OnInit, OnDestroy, Output, EventEmitter, Input } from '@angular/core';
-import { Transaction } from '../../interfaces/electrs.interface';
-import { MempoolPosition } from '../../interfaces/node-api.interface';
+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',
@@ -8,21 +12,243 @@ import { MempoolPosition } from '../../interfaces/node-api.interface';
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
- @Input() tx: Transaction ;
- @Input() eta: number;
+ @Input() eta: number = Date.now() + 123456789;
+ @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Output() close = new EventEmitter
();
- constructor() {
+ calculating = true;
+ choosenOption: 'wait' | 'accelerate' = 'wait';
+ 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;
+ step: 'cta' | 'checkout' | 'processing' | 'completed' = 'completed';
+
+ constructor(
+ private websocketService: WebsocketService,
+ private servicesApiService: ServicesApiServices,
+ 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.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() {
+ if (this.estimateSubscription) {
+ this.estimateSubscription.unsubscribe();
+ }
}
+ /**
+ * Accelerator
+ */
+ estimate() {
+ if (this.estimateSubscription) {
+ this.estimateSubscription.unsubscribe();
+ }
+ this.calculating = true;
+ this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe(
+ tap((response) => {
+ this.calculating = false;
+ if (response.status === 204) {
+ this.error = `cannot_accelerate_tx`;
+ } else {
+ const estimation = response.body;
+ if (!estimation) {
+ this.error = `cannot_accelerate_tx`;
+ return;
+ }
+ // Make min extra fee at least 50% of the current tx fee
+ const minExtraBoost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee));
+ const DEFAULT_BID_RATIO = 2;
+ this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
+ this.cost = this.maxBidBoost * DEFAULT_BID_RATIO + estimation.mempoolBaseFee + estimation.vsizeFee;
+ }
+ }),
+
+ catchError((response) => {
+ this.error = `cannot_accelerate_tx`;
+ return of(null);
+ })
+ ).subscribe();
+ }
+
+ /**
+ * Square
+ */
+ insertSquare(): void {
+ //@ts-ignore
+ if (window.Square) {
+ return;
+ }
+ let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
+ if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
+ document.location.hostname === 'mempool-staging.va1.mempool.space' ||
+ document.location.hostname === 'mempool-staging.fra.mempool.space' ||
+ document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
+ document.location.hostname === 'mempool.space') {
+ statsUrl = 'https://web.squarecdn.com/v1/square.js';
+ }
+
+ (function() {
+ const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ // @ts-ignore
+ g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
+ })();
+ }
+ setupSquare() {
+ const init = () => {
+ this.initSquare();
+ };
+
+ //@ts-ignore
+ if (!window.Square) {
+ console.debug('Square.js failed to load properly. Retrying in 1 second.');
+ setTimeout(init, 1000);
+ } else {
+ init();
+ }
+ }
+ async initSquare(): Promise {
+ try {
+ //@ts-ignore
+ this.payments = window.Square.payments(this.square.appId, this.square.locationId)
+ await this.requestCashAppPayment();
+ } catch (e) {
+ console.error(e);
+ this.error = 'Error loading Square Payments';
+ return;
+ }
+ }
+ async requestCashAppPayment() {
+ if (this.cashAppSubscription) {
+ this.cashAppSubscription.unsubscribe();
+ }
+ if (this.conversionsSubscription) {
+ this.conversionsSubscription.unsubscribe();
+ }
+
+ this.conversionsSubscription = this.stateService.conversions$.subscribe(
+ async (conversions) => {
+ if (this.cashAppPay) {
+ await this.cashAppPay.destroy();
+ }
+
+ const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
+ 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',
+ total: {
+ amount: costUSD.toString(),
+ label: 'Total',
+ pending: true,
+ productUrl: `${redirectHostname}/tracker/${this.txid}`,
+ },
+ button: { shape: 'semiround', size: 'small', theme: 'light'}
+ });
+ this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
+ 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'}
+ });
+
+ 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;
+ if (error) {
+ this.error = error;
+ } else if (tokenResult.status === 'OK') {
+ that.servicesApiService.accelerateWithCashApp$(
+ that.txid,
+ tokenResult.token,
+ tokenResult.details.cashAppPay.cashtag,
+ tokenResult.details.cashAppPay.referenceId,
+ that.accelerationUUID
+ ).subscribe({
+ next: () => {
+ that.audioService.playSound('ascend-chime-cartoon');
+ that.step = 'completed';
+ setTimeout(() => {
+ that.closeModal();
+ }, 10000);
+ },
+ error: (response) => {
+ if (response.status === 403 && response.error === 'not_available') {
+ that.error = 'waitlisted';
+ } else {
+ that.error = response.error;
+ }
+ }
+ });
+ }
+ });
+ }
+ );
+ }
+
+ /**
+ * UI events
+ */
+ enableCheckoutPage() {
+ 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.step = 'cta';
+ this.choosenOption = 'wait';
+ }
closeModal(): void {
- console.log('close modal')
+ if (this.cashAppPay) {
+ this.cashAppPay.destroy();
+ }
this.close.emit();
}
}
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts
index ca4e61c06..ec36107a4 100644
--- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts
+++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts
@@ -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,
diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts
index 45070ad67..6ed3d8cf6 100644
--- a/frontend/src/app/components/time/time.component.ts
+++ b/frontend/src/app/components/time/time.component.ts
@@ -23,7 +23,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() time: number;
@Input() dateString: number;
- @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' = 'plain';
+ @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain';
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() relative = false;
@@ -80,6 +80,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
break;
case 'until':
+ case 'within':
seconds = (+new Date(this.time) - +new Date()) / 1000;
break;
default:
@@ -91,7 +92,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
} else if (seconds < 60) {
if (this.relative || this.kind === 'since') {
return $localize`:@@date-base.just-now:Just now`;
- } else if (this.kind === 'until') {
+ } else if (this.kind === 'until' || this.kind === 'within') {
seconds = 60;
}
}
@@ -112,12 +113,12 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
if (counter > 0) {
let rounded;
const roundFactor = Math.pow(10,this.fractionDigits || 0);
- if (this.kind === 'until' && usedUnits < this.numUnits) {
+ if ((this.kind === 'until' || this.kind === 'within') && usedUnits < this.numUnits) {
rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
} else {
rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
}
- if (this.kind !== 'until' || this.numUnits === 1) {
+ if ((this.kind !== 'until' && this.kind !== 'within')|| this.numUnits === 1) {
return this.formatTime(this.kind, precisionUnit, rounded);
} else {
if (!usedUnits) {
@@ -185,6 +186,29 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
}
break;
+ case 'within':
+ if (number === 1) {
+ switch (unit) { // singular (In ~1 day)
+ case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break;
+ case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break;
+ case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break;
+ case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break;
+ case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break;
+ case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`;
+ case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`;
+ }
+ } else {
+ switch (unit) { // plural (In ~2 days)
+ case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break;
+ case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break;
+ case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break;
+ case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break;
+ case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break;
+ case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break;
+ case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break;
+ }
+ }
+ break;
case 'span':
if (number === 1) {
switch (unit) { // singular (1 day)
diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html
index 2d3d50d76..d83b7fd84 100644
--- a/frontend/src/app/components/tracker/tracker.component.html
+++ b/frontend/src/app/components/tracker/tracker.component.html
@@ -42,7 +42,7 @@
}
@if (isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
- Accelerate
+ Accelerate
}
@if (showAccelerationSummary) {
-
= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="showAccelerationSummary = false">
+
= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="showAccelerationSummary = false">
} @else {
@if (tx?.acceleration && !tx.status?.confirmed) {
diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts
index 2774dfd76..eb2a9b302 100644
--- a/frontend/src/app/components/tracker/tracker.component.ts
+++ b/frontend/src/app/components/tracker/tracker.component.ts
@@ -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();
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index 44d253efa..89ea9a603 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -132,8 +132,8 @@ export class ServicesApiServices {
return this.httpClient.post
(`${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(`${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(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
}
getAccelerations$(): Observable {