feat(checkout): "Open in wallet" deeplink + gate LNbits-pay on having a wallet

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:<bolt11>`. 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) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-11 22:46:48 +02:00
commit 05d09b30c8
2 changed files with 55 additions and 12 deletions

View file

@ -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
</span>
</div>
<Button
variant="outline"
size="sm"
class="w-full font-mono text-xs"
@click="copy"
>
<Check v-if="copied" class="mr-2 h-3.5 w-3.5" />
<Copy v-else class="mr-2 h-3.5 w-3.5" />
{{ copied ? 'Copied' : 'Copy invoice' }}
</Button>
<div class="flex gap-2">
<Button
variant="outline"
size="sm"
class="flex-1 font-mono text-xs"
@click="copy"
>
<Check v-if="copied" class="mr-2 h-3.5 w-3.5" />
<Copy v-else class="mr-2 h-3.5 w-3.5" />
{{ copied ? 'Copied' : 'Copy' }}
</Button>
<Button
variant="default"
size="sm"
class="flex-1 text-xs"
@click="openInWallet"
>
<Zap class="mr-2 h-3.5 w-3.5" />
Open in wallet
</Button>
</div>
</CardContent>
</Card>
</template>

View file

@ -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<StorageService>(SERVICE_TOKENS.STORAGE_SERVICE)
const api = injectService<RestaurantAPI>(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) {
</Alert>
<Button
v-if="!allPaid"
v-if="!allPaid && canPayFromLnbits"
size="lg"
class="w-full"
:disabled="isPaying"
@ -438,6 +450,14 @@ function buildOrderInvoice(p: PlacedOrder) {
Pay from my LNbits wallet
</Button>
<p
v-else-if="!allPaid"
class="text-center text-xs text-muted-foreground"
>
Use the "Open in wallet" button above to pay with any
Lightning wallet on your phone, or scan the QR.
</p>
<Button
v-if="!allPaid"
variant="ghost"