fix(checkout): pay via LNbits payments API directly (drop WalletService dep)
useCheckout was injecting SERVICE_TOKENS.WALLET_SERVICE, but the
restaurant-app bundle only registers base + restaurant modules —
the wallet module isn't bundled because the restaurant surface is
a customer ordering app, not a wallet UI. Result was a hard
'Service not found for token: Symbol(walletService)' on every
checkout setup, which then cascaded into 'Cannot read properties
of undefined (reading 'grandTotal')' downstream.
Two options to fix: (a) bundle the whole wallet module, (b) talk
to LNbits's payments endpoint directly. Going with (b) to match
the market bundle's 'no wallet UI' pattern.
Changes in useCheckout:
- drop the WalletService import + injectService call
- new local payBolt11(bolt11, adminkey) helper that POSTs to
`${apiBaseUrl}/api/v1/payments` with X-Api-Key and
{ out: true, bolt11 } — same shape WalletService.sendPayment
builds internally
- balance precheck now reads
AuthService.user.wallets[0].balance_msat (which LNbits's user
object carries natively) instead of WalletService.balance.
Comparison is msat-on-msat — no more sat→msat ×1000 dance
- explicit 'No wallet available — please log in first.' error
when the user object lacks wallets[0].adminkey
- per-bolt11 failure now surfaces the real LNbits error text
instead of WalletService's boolean swallow
Verified vue-tsc clean. The 'Invalid vnode type' and 'undefined
grandTotal' errors were both downstream of this setup failure;
they go away when setup completes.
This commit is contained in:
parent
f2045c511d
commit
10abfca555
1 changed files with 67 additions and 21 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
import { ref, type Ref } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import type WalletService from '@/modules/wallet/services/WalletService'
|
import appConfig from '@/app.config'
|
||||||
import type { RestaurantAPI } from '../services/RestaurantAPI'
|
import type { RestaurantAPI } from '../services/RestaurantAPI'
|
||||||
import { useCartStore, type CartLine } from '../stores/cart'
|
import { useCartStore, type CartLine } from '../stores/cart'
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -90,10 +90,44 @@ function buildCreateOrder(
|
||||||
|
|
||||||
export function useCheckout(): UseCheckoutReturn {
|
export function useCheckout(): UseCheckoutReturn {
|
||||||
const api = injectService<RestaurantAPI>(SERVICE_TOKENS.RESTAURANT_API)
|
const api = injectService<RestaurantAPI>(SERVICE_TOKENS.RESTAURANT_API)
|
||||||
const wallet = injectService<WalletService>(SERVICE_TOKENS.WALLET_SERVICE)
|
|
||||||
const cart = useCartStore()
|
const cart = useCartStore()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
// We talk to LNbits's payments endpoint directly rather than
|
||||||
|
// pulling in the whole `wallet` module — the restaurant-app
|
||||||
|
// bundle is a customer surface, not a wallet UI, and `LnbitsAPI`
|
||||||
|
// is already registered by base. The customer's adminkey lives
|
||||||
|
// on AuthService.user.wallets[0].adminkey.
|
||||||
|
const apiBaseUrl =
|
||||||
|
(
|
||||||
|
appConfig.modules.restaurant as
|
||||||
|
| { config?: { apiBaseUrl?: string } }
|
||||||
|
| undefined
|
||||||
|
)?.config?.apiBaseUrl || ''
|
||||||
|
|
||||||
|
async function payBolt11(bolt11: string, adminkey: string): Promise<void> {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/api/v1/payments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Api-Key': adminkey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ out: true, bolt11 }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
let detail = response.statusText
|
||||||
|
try {
|
||||||
|
const body = await response.json()
|
||||||
|
if (body?.detail) detail = body.detail
|
||||||
|
} catch {
|
||||||
|
/* body wasn't JSON */
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Payment failed: ${response.status} ${detail}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const state = ref<CheckoutState>({
|
const state = ref<CheckoutState>({
|
||||||
step: 'idle',
|
step: 'idle',
|
||||||
progress: { current: 0, total: 0 },
|
progress: { current: 0, total: 0 },
|
||||||
|
|
@ -141,27 +175,32 @@ export function useCheckout(): UseCheckoutReturn {
|
||||||
quotes.push({ ...b, msat: quote.required_msat })
|
quotes.push({ ...b, msat: quote.required_msat })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Pre-flight balance check. The webapp's WalletService
|
// 2. Pre-flight balance check using AuthService's cached wallet
|
||||||
// exposes balance via PaymentService; we ask the service for
|
// balance (LNbits's user object carries balance_msat per wallet).
|
||||||
// the current cached value via its public computed.
|
|
||||||
// NOTE: balance values in this codebase are sats, not msat —
|
|
||||||
// convert before comparing.
|
|
||||||
const totalMsatRequired = quotes.reduce((s, q) => s + q.msat, 0)
|
const totalMsatRequired = quotes.reduce((s, q) => s + q.msat, 0)
|
||||||
const balanceSat = (
|
const wallet0 = user.value?.wallets?.[0]
|
||||||
wallet as unknown as { balance?: { value?: number } }
|
if (wallet0 && typeof wallet0.balance_msat === 'number') {
|
||||||
).balance?.value
|
if (wallet0.balance_msat < totalMsatRequired) {
|
||||||
if (typeof balanceSat === 'number') {
|
const needSat = Math.ceil(totalMsatRequired / 1000)
|
||||||
const balanceMsat = balanceSat * 1000
|
const haveSat = Math.floor(wallet0.balance_msat / 1000)
|
||||||
if (balanceMsat < totalMsatRequired) {
|
|
||||||
state.value = {
|
state.value = {
|
||||||
step: 'error',
|
step: 'error',
|
||||||
progress: { current: 0, total: buckets.length },
|
progress: { current: 0, total: buckets.length },
|
||||||
currentRestaurantSlug: null,
|
currentRestaurantSlug: null,
|
||||||
error: `Insufficient balance. Need ${Math.ceil(totalMsatRequired / 1000)} sat, have ${balanceSat} sat.`,
|
error: `Insufficient balance. Need ${needSat} sat, have ${haveSat} sat.`,
|
||||||
}
|
}
|
||||||
throw new Error(state.value.error!)
|
throw new Error(state.value.error!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!wallet0?.adminkey) {
|
||||||
|
state.value = {
|
||||||
|
step: 'error',
|
||||||
|
progress: { current: 0, total: buckets.length },
|
||||||
|
currentRestaurantSlug: null,
|
||||||
|
error: 'No wallet available — please log in first.',
|
||||||
|
}
|
||||||
|
throw new Error(state.value.error!)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Place orders.
|
// 3. Place orders.
|
||||||
state.value.step = 'placing'
|
state.value.step = 'placing'
|
||||||
|
|
@ -196,13 +235,20 @@ export function useCheckout(): UseCheckoutReturn {
|
||||||
state.value.progress = { current: i, total: placed.length }
|
state.value.progress = { current: i, total: placed.length }
|
||||||
if (!p.invoice) continue // cash orders skip payment
|
if (!p.invoice) continue // cash orders skip payment
|
||||||
|
|
||||||
const success = await wallet.sendPayment({
|
try {
|
||||||
amount: Math.ceil(p.invoice.amount_msat / 1000),
|
await payBolt11(p.invoice.bolt11, wallet0.adminkey)
|
||||||
destination: p.invoice.bolt11,
|
if (p.invoice.payment_hash) {
|
||||||
comment: `Order at ${p.restaurantSlug}`,
|
paidHashes.push(p.invoice.payment_hash)
|
||||||
})
|
}
|
||||||
if (success && p.invoice.payment_hash) {
|
} catch (err) {
|
||||||
paidHashes.push(p.invoice.payment_hash)
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
state.value = {
|
||||||
|
step: 'error',
|
||||||
|
progress: { current: i, total: placed.length },
|
||||||
|
currentRestaurantSlug: p.restaurantSlug,
|
||||||
|
error: msg,
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue