From 2869ed66776007ad87098ec7a69c2308f156244e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 20 Aug 2026 12:24:54 -0600 Subject: [PATCH] Subscribe to zap receipt when zapping, show success for external payment --- app/components/zap-photo-modal.gjs | 55 ++++++++++++++++++- app/services/nostr-zap.js | 33 +++++++++++ tests/helpers/mock-nostr.js | 22 ++++++++ .../components/zap-photo-modal-test.gjs | 29 ++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/app/components/zap-photo-modal.gjs b/app/components/zap-photo-modal.gjs index 60ee74c..4d3df46 100644 --- a/app/components/zap-photo-modal.gjs +++ b/app/components/zap-photo-modal.gjs @@ -47,10 +47,17 @@ export default class ZapPhotoModal extends Component { @tracked lnurlEndpoint = null; @tracked lnurlLoading = false; @tracked lnurlError = null; + @tracked paymentNotDetected = false; sliderSteps = SLIDER_STEPS; sliderTicks = ['10', '100', '1k', '10k', '100k']; + _receiptSub = null; + _receiptTimeout = null; + _zapRequest = null; + + RECEIPT_TIMEOUT_MS = 5 * 60 * 1000; + constructor() { super(...arguments); this._loadLightningInfo(); @@ -162,9 +169,11 @@ export default class ZapPhotoModal extends Component { @action resetAndClose() { + this._cleanupReceiptSub(); this.step = 'select-amount'; this.error = null; this.invoice = null; + this.paymentNotDetected = false; this.handleClose(); } @@ -206,6 +215,36 @@ export default class ZapPhotoModal extends Component { e.stopPropagation(); } + _startReceiptSubscription(zapRequest) { + this._cleanupReceiptSub(); + this._zapRequest = zapRequest; + this.paymentNotDetected = false; + + this._receiptSub = this.nostrZap.subscribeForZapReceipt( + zapRequest, + this.photo, + () => { + this.step = 'success'; + this._cleanupReceiptSub(); + } + ); + + this._receiptTimeout = setTimeout(() => { + this.paymentNotDetected = true; + }, this.RECEIPT_TIMEOUT_MS); + } + + _cleanupReceiptSub() { + if (this._receiptSub) { + this._receiptSub.unsubscribe(); + this._receiptSub = null; + } + if (this._receiptTimeout) { + clearTimeout(this._receiptTimeout); + this._receiptTimeout = null; + } + } + zapTask = task(async () => { if (!this.canZap) return; @@ -213,13 +252,14 @@ export default class ZapPhotoModal extends Component { this.error = null; try { - const { invoice } = await this.nostrZap.zap( + const { invoice, zapRequest } = await this.nostrZap.zap( this.photo, this.amountMsats, this.message ); this.invoice = invoice; this.step = 'awaiting-payment'; + this._startReceiptSubscription(zapRequest); } catch (err) { console.error('Zap failed:', err); this.error = err instanceof Error ? err.message : 'Failed to create zap'; @@ -235,6 +275,7 @@ export default class ZapPhotoModal extends Component { try { await this.nostrZap.payWithWebln(this.invoice); + this._cleanupReceiptSub(); this.step = 'success'; } catch (err) { console.error('WebLN pay failed:', err); @@ -244,6 +285,11 @@ export default class ZapPhotoModal extends Component { } }); + willDestroy() { + this._cleanupReceiptSub(); + super.willDestroy(...arguments); + } +