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 { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import QRCode from 'qrcode'
|
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 { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import type { OrderInvoice } from '../types/restaurant'
|
import type { OrderInvoice } from '../types/restaurant'
|
||||||
|
|
@ -58,6 +58,18 @@ function copy() {
|
||||||
setTimeout(() => (copied.value = false), 1500)
|
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) {
|
function fmtCountdown(seconds: number) {
|
||||||
const m = Math.floor(seconds / 60)
|
const m = Math.floor(seconds / 60)
|
||||||
const s = seconds % 60
|
const s = seconds % 60
|
||||||
|
|
@ -89,16 +101,27 @@ function fmtCountdown(seconds: number) {
|
||||||
{{ Math.ceil(invoice.amount_msat / 1000) }} sat
|
{{ Math.ceil(invoice.amount_msat / 1000) }} sat
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full font-mono text-xs"
|
class="flex-1 font-mono text-xs"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
>
|
>
|
||||||
<Check v-if="copied" class="mr-2 h-3.5 w-3.5" />
|
<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" />
|
<Copy v-else class="mr-2 h-3.5 w-3.5" />
|
||||||
{{ copied ? 'Copied' : 'Copy invoice' }}
|
{{ copied ? 'Copied' : 'Copy' }}
|
||||||
</Button>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import OrderInvoiceCard from '../components/OrderInvoiceCard.vue'
|
||||||
import { useCartStore } from '../stores/cart'
|
import { useCartStore } from '../stores/cart'
|
||||||
import { useCheckout, type PlacedOrder } from '../composables/useCheckout'
|
import { useCheckout, type PlacedOrder } from '../composables/useCheckout'
|
||||||
import { friendlyOrderStatus } from '../types/restaurant'
|
import { friendlyOrderStatus } from '../types/restaurant'
|
||||||
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import {
|
import {
|
||||||
injectService,
|
injectService,
|
||||||
tryInjectService,
|
tryInjectService,
|
||||||
|
|
@ -60,6 +61,17 @@ const cart = useCartStore()
|
||||||
const { state, placeOrders, payAll, reset } = useCheckout()
|
const { state, placeOrders, payAll, reset } = useCheckout()
|
||||||
const storage = tryInjectService<StorageService>(SERVICE_TOKENS.STORAGE_SERVICE)
|
const storage = tryInjectService<StorageService>(SERVICE_TOKENS.STORAGE_SERVICE)
|
||||||
const api = injectService<RestaurantAPI>(SERVICE_TOKENS.RESTAURANT_API)
|
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 //
|
// review phase //
|
||||||
|
|
@ -427,7 +439,7 @@ function buildOrderInvoice(p: PlacedOrder) {
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
v-if="!allPaid"
|
v-if="!allPaid && canPayFromLnbits"
|
||||||
size="lg"
|
size="lg"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="isPaying"
|
:disabled="isPaying"
|
||||||
|
|
@ -438,6 +450,14 @@ function buildOrderInvoice(p: PlacedOrder) {
|
||||||
Pay from my LNbits wallet
|
Pay from my LNbits wallet
|
||||||
</Button>
|
</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
|
<Button
|
||||||
v-if="!allPaid"
|
v-if="!allPaid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue