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:
parent
1651f4b2f1
commit
05d09b30c8
2 changed files with 55 additions and 12 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue