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') { + +
+

Confirm your payment

@@ -76,36 +88,40 @@ - @if (!loadingCashapp) { + @if (paymentMethod === 'cashapp') { + @if (!loadingCashapp) { +
+
+
+ Total additional cost
+ + Pay + + with + +
+
+
+
+ } +
- Total additional cost
- - Pay - - with - -
+
+ @if (loadingCashapp) { +
+ Loading payment method... +
+
+ }
+ } @else if (paymentMethod === 'btcpay' && invoice?.btcpayInvoiceId) { + } -
-
-
-
- @if (loadingCashapp) { -
- Loading payment method... -
-
- } -
-
-
-
@@ -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) { - + } @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,