From 05d09b30c85d3f161aee736c1babf8692ad41aeb Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 11 May 2026 22:46:48 +0200 Subject: [PATCH] feat(checkout): "Open in wallet" deeplink + gate LNbits-pay on having a wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Pay from my LNbits wallet" CTA was shown unconditionally — but it only works when the customer is logged in AND has a wallet with an admin key (both required for the POST /api/v1/payments call). Hide it otherwise and surface a hint pointing at the new deeplink-and-QR path instead. Add an "Open in wallet" button next to "Copy" in OrderInvoiceCard that navigates to `lightning:`. Mobile OSes route this URI to the user's default Lightning wallet (Phoenix, Zeus, Wallet of Satoshi, etc.), so a customer without an LNbits account can still pay end-to-end from the same checkout surface. Even authenticated users benefit — they may prefer their own wallet over the LNbits-internal flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/OrderInvoiceCard.vue | 45 ++++++++++++++----- src/modules/restaurant/views/CheckoutPage.vue | 22 ++++++++- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/modules/restaurant/components/OrderInvoiceCard.vue b/src/modules/restaurant/components/OrderInvoiceCard.vue index 64d613b..c94c8fe 100644 --- a/src/modules/restaurant/components/OrderInvoiceCard.vue +++ b/src/modules/restaurant/components/OrderInvoiceCard.vue @@ -5,7 +5,7 @@ */ import { onMounted, onUnmounted, ref, watch } from 'vue' import QRCode from 'qrcode' -import { Copy, Check } from 'lucide-vue-next' +import { Copy, Check, Zap } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import type { OrderInvoice } from '../types/restaurant' @@ -58,6 +58,18 @@ function copy() { setTimeout(() => (copied.value = false), 1500) } +/** + * The `lightning:` URI scheme is the cross-platform handoff to a + * native LN wallet — iOS / Android route it to whichever wallet the + * user has set as default (Phoenix, Zeus, Wallet of Satoshi, etc.). + * Desktop browsers usually no-op or prompt to pick an app; we still + * surface the button so mobile-web flows work end-to-end without an + * LNbits account. + */ +function openInWallet() { + window.location.href = `lightning:${props.invoice.bolt11}` +} + function fmtCountdown(seconds: number) { const m = Math.floor(seconds / 60) const s = seconds % 60 @@ -89,16 +101,27 @@ function fmtCountdown(seconds: number) { {{ Math.ceil(invoice.amount_msat / 1000) }} sat - +
+ + +
diff --git a/src/modules/restaurant/views/CheckoutPage.vue b/src/modules/restaurant/views/CheckoutPage.vue index 096ee85..ab831e8 100644 --- a/src/modules/restaurant/views/CheckoutPage.vue +++ b/src/modules/restaurant/views/CheckoutPage.vue @@ -47,6 +47,7 @@ import OrderInvoiceCard from '../components/OrderInvoiceCard.vue' import { useCartStore } from '../stores/cart' import { useCheckout, type PlacedOrder } from '../composables/useCheckout' import { friendlyOrderStatus } from '../types/restaurant' +import { useAuth } from '@/composables/useAuthService' import { injectService, tryInjectService, @@ -60,6 +61,17 @@ const cart = useCartStore() const { state, placeOrders, payAll, reset } = useCheckout() const storage = tryInjectService(SERVICE_TOKENS.STORAGE_SERVICE) const api = injectService(SERVICE_TOKENS.RESTAURANT_API) +const { user } = useAuth() + +// The LNbits "pay from my wallet" CTA only makes sense when the +// customer is logged in *and* has a wallet with an admin key — both +// are required for the POST /api/v1/payments call. Anonymous +// customers (and logged-in users without a wallet) still get the QR +// + "Open in wallet" deeplink in OrderInvoiceCard, which routes to a +// native LN wallet on mobile. +const canPayFromLnbits = computed( + () => !!user.value?.wallets?.[0]?.adminkey +) // ----------------------------------------------------------------- // // review phase // @@ -427,7 +439,7 @@ function buildOrderInvoice(p: PlacedOrder) {