-
-
-
+
+
+
+
+ Changed your mind?
+
-
+ }
\ No newline at end of file
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 e69de29bb..395b14d25 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
@@ -0,0 +1,3 @@
+.estimating {
+ color: var(--green)
+}
\ No newline at end of file
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 0591e58a2..1576c173b 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
@@ -1,4 +1,9 @@
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Component, OnInit, OnDestroy, Input } 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';
@Component({
selector: 'app-accelerate-checkout',
@@ -6,12 +11,224 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
- constructor() {
- }
+ @Input() eta: number = Date.now() + 123456789;
+ @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
+
+ calculating = true;
+ choosenOption: 'wait' | 'accelerate' = 'wait';
+ showCheckoutPage = false;
+ error = '';
+
+ // accelerator stuff
+ square: { appId: string, locationId: string};
+ accelerationUUID: string;
+ estimateSubscription: Subscription;
+ cost: number; // sats
+
+ // square
+ cashappSubmit: any;
+ payments: any;
+ cashAppPay: any;
+ cashAppSubscription: Subscription;
+ conversionsSubscription: Subscription;
+ loadingCashapp = true;
+ processingPayment = true;
+
+ constructor(
+ private websocketService: WebsocketService,
+ private servicesApiService: ServicesApiServices,
+ private stateService: StateService
+ ) {}
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();
+ });
+ }
}
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 minExtraCost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee));
+ const DEFAULT_BID_RATIO = 2;
+ this.cost = minExtraCost * 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() {
+ this.loadingCashapp = true;
+
+ if (this.cashAppSubscription) {
+ this.cashAppSubscription.unsubscribe();
+ }
+ if (this.conversionsSubscription) {
+ this.conversionsSubscription.unsubscribe();
+ }
+
+ this.conversionsSubscription = this.stateService.conversions$.subscribe(
+ async (conversions) => {
+ if (this.cashAppPay) {
+ 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 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}?acceleration=false`,
+ 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 });
+
+ 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,
+ that.cost,
+ tokenResult.token,
+ tokenResult.details.cashAppPay.cashtag,
+ tokenResult.details.cashAppPay.referenceId,
+ that.accelerationUUID
+ ).subscribe({
+ next: () => {
+ that.estimateSubscription.unsubscribe();
+ },
+ error: (response) => {
+ if (response.status === 403 && response.error === 'not_available') {
+ that.error = 'waitlisted';
+ } else {
+ that.error = response.error;
+ }
+ }
+ });
+ }
+ });
+ this.loadingCashapp = false;
+ }
+ );
+ }
+ submitCashappPay(): void {
+ if (this.cashappSubmit) {
+ this.cashappSubmit?.begin();
+ this.processingPayment = true;
+ }
+ }
+
+ /**
+ * UI events
+ */
+ enableCheckoutPage() {
+ this.showCheckoutPage = true;
+ this.insertSquare();
+ this.setupSquare();
+ }
+ selectedOptionChanged(event) {
+ this.choosenOption = event.target.id;
+ }
+ restart() {
+ this.showCheckoutPage = false
+ this.choosenOption = 'wait';
}
}
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)