diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html
index 8f82fe69c..40089ddbf 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html
@@ -58,12 +58,24 @@
- }
-
- @else if (step === 'checkout') {
-
+
+ } @else if (step === 'paymentMethod') {
+
Select your payment method
+
+
+
+ @if (cashappEnabled) {
+

+ }
+

+
+
+ } @else if (step === 'checkout') {
+
+
@@ -118,7 +134,7 @@
@else if (step === 'processing') {
-
Confirm your payment
+ Confirming 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 315bdbbd2..268f03f93 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss
@@ -7,3 +7,11 @@
.estimating {
color: var(--green)
}
+
+.paymentMethod {
+ padding: 10px;
+ background-color: var(--secondary);
+ border-radius: 15px;
+ border: 2px solid var(--bg);
+ cursor: pointer;
+}
\ 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 16400a70a..ba867d096 100644
--- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
+++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts
@@ -16,12 +16,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number | null = null;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Input() scrollEvent: boolean;
+ @Input() cashappEnabled: boolean;
@Output() close = new EventEmitter
();
calculating = true;
choosenOption: 'wait' | 'accelerate' = 'wait';
error = '';
+ step: 'paymentMethod' | 'cta' | 'checkout' | 'processing' = 'cta';
+ paymentMethod: 'cashapp' | 'btcpay';
+
// accelerator stuff
square: { appId: string, locationId: string};
accelerationUUID: string;
@@ -38,7 +42,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
cashAppPay: any;
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
- step: 'cta' | 'checkout' | 'processing' = 'cta';
+
+ // btcpay
+ loadingBtcpayInvoice = false;
+ invoice = undefined;
constructor(
private servicesApiService: ServicesApiServices,
@@ -77,19 +84,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
- this.scrollToPreview('acceleratePreviewAnchor', 'start');
+ this.scrollToElement('acceleratePreviewAnchor', 'start');
}
}
/**
* Scroll to element id with or without setTimeout
*/
- scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
+ scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000) {
setTimeout(() => {
- this.scrollToPreview(id, position);
- }, 1000);
+ this.scrollToElement(id, position);
+ }, timeout);
}
- scrollToPreview(id: string, position: ScrollLogicalPosition) {
+ scrollToElement(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
@@ -111,7 +118,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
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 {
@@ -126,6 +132,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate);
+ this.calculating = false;
+ this.cd.markForCheck();
}
}),
@@ -265,19 +273,48 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
);
}
+ /**
+ * BTCPay
+ */
+ async requestBTCPayInvoice() {
+ this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.txid).subscribe({
+ next: (response) => {
+ this.invoice = response;
+ this.cd.markForCheck();
+ this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 500);
+ },
+ error: (response) => {
+ console.log(response);
+ }
+ });
+ }
+
/**
* UI events
*/
enableCheckoutPage() {
+ this.step = 'paymentMethod';
+ }
+ selectPaymentMethod(paymentMethod: 'cashapp' | 'btcpay') {
this.step = 'checkout';
- this.loadingCashapp = true;
- this.insertSquare();
- this.setupSquare();
+ this.paymentMethod = paymentMethod;
+ if (paymentMethod === 'cashapp') {
+ this.loadingCashapp = true;
+ this.insertSquare();
+ this.setupSquare();
+ } else if (paymentMethod === 'btcpay') {
+ this.loadingBtcpayInvoice = true;
+ this.requestBTCPayInvoice();
+ }
}
selectedOptionChanged(event) {
this.choosenOption = event.target.id;
}
- closeModal(): void {
- this.close.emit();
+ closeModal(timeout: number = 0): void {
+ setTimeout(() => {
+ this.step = 'processing';
+ this.cd.markForCheck();
+ this.close.emit();
+ }, timeout);
}
}
diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html
new file mode 100644
index 000000000..dabaf991e
--- /dev/null
+++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html
@@ -0,0 +1,89 @@
+
+
+
+ Payment successful. You can close this page.
+
+
+
+ A transaction has been detected in the mempool fully paying for this invoice. Waiting for on-chain confirmation.
+
+
+
+
+
+
+
+
+
+
+
+ {{ invoice.amount }} BTC
+
+
+
+
+
+
+
+
+
+ {{ invoice.amount * 100_000_000 }} sats
+
+
+
+
+
+
+
+
+ {{ invoice.amount }} BTC
+
+
+
+
Waiting for transaction...
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss
new file mode 100644
index 000000000..7582b70f0
--- /dev/null
+++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss
@@ -0,0 +1,149 @@
+.form-panel {
+ background-color: #292b45;
+ padding: 20px;
+}
+
+
+.sponsor-page {
+ text-align: center;
+}
+
+.qr-wrapper {
+ background-color: #FFF;
+ padding: 10px;
+ display: inline-block;
+ padding-bottom: 5px;
+ margin: 20px auto 0px;
+}
+
+.info-group {
+ max-width: 400px;
+}
+
+.card {
+ width: 240px;
+ height: 220px;
+ background-color: var(--bg);
+ border: 2px solid var(--bg);
+ cursor: pointer;
+ position: relative;
+ transition: 100ms all;
+ margin: 30px 30px 20px 30px;
+ @media(min-width: 476px) {
+ margin: 30px 100px 20px 100px;
+ }
+ @media(min-width: 851px) {
+ margin: 60px 20px 40px 20px;
+ }
+
+ .card-title {
+ font-weight: bold;
+ span {
+ font-weight: 100;
+ }
+ }
+
+ &.bigger {
+ height: 220px;
+ width: 240px;
+ margin-top: 40px;
+ }
+
+ &:hover {
+ background-color: #5058926b;
+ border: 2px solid #505892;
+ transform: scale(1.1) translateY(-10px);
+ margin-top: 70px;
+
+ .card-header {
+ background-color: #505892;
+ }
+ }
+}
+
+.donation-form {
+ max-width: 280px;
+ margin: auto;
+ button {
+ width: 100%;
+ }
+}
+
+.card-header {
+ background-color: #171929;
+}
+
+.flex-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.middle-card {
+ width: 280px;
+ height: 260px;
+ margin-top: 40px;
+ &:hover {
+ margin-top: 50px;
+ }
+}
+
+.shiny-border {
+ background-color: #5058926b;
+ border: 2px solid #505892;
+ transform: scale(1.1) translateY(-10px);
+ margin-top: 70px;
+ box-shadow: 0px 0px 100px #9858ff52;
+ .card-header {
+ background-color: #505892;
+ }
+
+ &.middle-card {
+ margin-top: 50px;
+ }
+}
+
+.input-group {
+ margin: 20px auto;
+}
+
+.donation-confirmed {
+ h2 {
+ margin-top: 50px;
+ span {
+ display: block;
+ &:last-child {
+ color: #9858ff;
+ font-weight: bold;
+ font-size: 2rem;
+ }
+ }
+ }
+
+ .order-details {
+ margin-top: 50px;
+ span {
+ color: #d81b60;
+ margin-left: 10px;
+ }
+ }
+}
+
+.card-body {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ height: 100%;
+}
+
+.wrapper {
+ text-align: center;
+}
+
+.input-dark {
+ background-color: var(--bg);
+ border-color: var(--active-bg);
+ color: white;
+}
diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts
new file mode 100644
index 000000000..2e12f54ba
--- /dev/null
+++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts
@@ -0,0 +1,94 @@
+import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
+import { ActivatedRoute } from '@angular/router';
+import { Subscription, timer } from 'rxjs';
+import { retry, switchMap, tap } from 'rxjs/operators';
+import { ServicesApiServices } from '../../services/services-api.service';
+
+@Component({
+ selector: 'app-bitcoin-invoice',
+ templateUrl: './bitcoin-invoice.component.html',
+ styleUrls: ['./bitcoin-invoice.component.scss']
+})
+export class BitcoinInvoiceComponent implements OnInit, OnDestroy {
+ @Input() invoiceId: string;
+ @Input() redirect = true;
+ @Output() completed = new EventEmitter();
+
+ paymentForm: FormGroup;
+ requestSubscription: Subscription | undefined;
+ paymentStatusSubscription: Subscription | undefined;
+ invoice: any;
+ paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
+ paramMapSubscription: Subscription | undefined;
+ invoiceSubscription: Subscription | undefined;
+ invoiceTimeout; // Wait for angular to load all the things before making a request
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private apiService: ServicesApiServices,
+ private sanitizer: DomSanitizer,
+ private activatedRoute: ActivatedRoute
+ ) { }
+
+ ngOnDestroy() {
+ if (this.requestSubscription) {
+ this.requestSubscription.unsubscribe();
+ }
+ if (this.paramMapSubscription) {
+ this.paramMapSubscription.unsubscribe();
+ }
+ if (this.invoiceSubscription) {
+ this.invoiceSubscription.unsubscribe();
+ }
+ if (this.paymentStatusSubscription) {
+ this.paymentStatusSubscription.unsubscribe();
+ }
+ }
+
+ ngOnInit(): void {
+ this.paymentForm = this.formBuilder.group({
+ 'method': 'lightning'
+ });
+
+ /**
+ * If the invoice is passed in the url, fetch it and display btcpay payment
+ * Otherwise get a new invoice
+ */
+ this.paramMapSubscription = this.activatedRoute.paramMap
+ .pipe(
+ tap((paramMap) => {
+ const invoiceId = paramMap.get('invoiceId') ?? this.invoiceId;
+ if (invoiceId) {
+ this.paymentStatusSubscription = this.apiService.retreiveInvoice$(invoiceId).pipe(
+ tap((invoice: any) => {
+ this.invoice = invoice;
+ this.invoice.amount = invoice.btcDue ?? (invoice.cryptoInfo.length ? parseFloat(invoice.cryptoInfo[0].totalDue) : 0) ?? 0;
+
+ if (this.invoice.amount > 0) {
+ this.paymentStatus = 2;
+ } else {
+ this.paymentStatus = 4;
+ }
+ }),
+ switchMap(() => this.apiService.getPaymentStatus$(this.invoice.id)
+ .pipe(
+ retry({ delay: () => timer(2000)})
+ )
+ ),
+ ).subscribe({
+ next: ((result) => {
+ this.paymentStatus = 3;
+ this.completed.emit();
+ }),
+ });
+ }
+ })
+ ).subscribe();
+ }
+
+ bypassSecurityTrustUrl(text: string): SafeUrl {
+ return this.sanitizer.bypassSecurityTrustUrl(text);
+ }
+}
diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html
index 571c02f96..1380990df 100644
--- a/frontend/src/app/components/tracker/tracker.component.html
+++ b/frontend/src/app/components/tracker/tracker.component.html
@@ -75,7 +75,7 @@
} @else {
}
- @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
+ @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
Accelerate
}
@@ -116,7 +116,7 @@
@if (showAccelerationSummary && !accelerationFlowCompleted) {
-
= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100">
+
= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100">
} @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 fe42ef0dd..508c8db19 100644
--- a/frontend/src/app/components/tracker/tracker.component.ts
+++ b/frontend/src/app/components/tracker/tracker.component.ts
@@ -107,7 +107,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
now = Date.now();
da$: Observable
;
isMobile: boolean;
- paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
trackerStage: TrackerStage = 'waiting';
@@ -158,9 +157,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
- if (this.acceleratorAvailable && this.stateService.referrer === 'https://cash.app/') {
- this.paymentType = 'cashapp';
- }
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) {
this.showAccelerationSummary = true;
@@ -390,11 +386,9 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.trackerStage = 'replaced';
}
+ this.showAccelerationSummary = true;
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.accelerationEligible = true;
- if (this.acceleratorAvailable && this.paymentType === 'cashapp') {
- this.showAccelerationSummary = true;
- }
}
}
} else {
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index bdc6d18c2..534f45b4e 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -167,4 +167,19 @@ export class ServicesApiServices {
requestTestnet4Coins$(address: string, sats: number) {
return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
}
+
+ generateBTCPayAcceleratorInvoice$(txid: string): Observable {
+ const params = {
+ product: txid
+ };
+ return this.httpClient.post(`${SERVICES_API_PREFIX}/payments/bitcoin`, params);
+ }
+
+ retreiveInvoice$(invoiceId: string): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`);
+ }
+
+ getPaymentStatus$(orderId: string): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`);
+ }
}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 2f7bd4dc4..e3f219aba 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -115,6 +115,7 @@ import { HttpErrorComponent } from '../shared/components/http-error/http-error.c
import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component';
import { FaucetComponent } from '../components/faucet/faucet.component';
import { TwitterLogin } from '../components/twitter-login/twitter-login.component';
+import { BitcoinInvoiceComponent } from '../components/bitcoin-invoice/bitcoin-invoice.component';
import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
@@ -230,6 +231,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
TwitterWidgetComponent,
FaucetComponent,
TwitterLogin,
+ BitcoinInvoiceComponent,
],
imports: [
CommonModule,
@@ -359,6 +361,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
HttpErrorComponent,
TwitterWidgetComponent,
TwitterLogin,
+ BitcoinInvoiceComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,