From 6779d8ef557f01fd8bac38f2f3cbcabff05ad597 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 20 Sep 2026 17:07:17 +0200 Subject: [PATCH] feat(machine): show the card holder's balance, hidden by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tap-to-enter session is effectively the holder logging into their card wallet, so show its balance — but a kiosk in a public place must not display a stranger's balance unasked. CardChip renders the card label with the balance masked (••••••) and an eye toggle; revealed, it mirrors the LNbits wallet page: sats, then the fiat equivalent formatted with Intl currency style. Fiat comes from the card server (the wallet's own currency, else the instance default, at its rate) and falls back to the ATM's fiat at its display rate when the server priced nothing. Shown on the idle menu and both cash screens; reveal state resets on re-lock. Co-Authored-By: Claude Fable 5.1 --- apps/machine/src/components/CardChip.vue | 78 ++++++++++++++++++++++++ apps/machine/src/stores/atm.ts | 27 ++++++++ apps/machine/src/views/CashInView.vue | 5 +- apps/machine/src/views/CashOutView.vue | 5 +- apps/machine/src/views/IdleView.vue | 3 + 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 apps/machine/src/components/CardChip.vue diff --git a/apps/machine/src/components/CardChip.vue b/apps/machine/src/components/CardChip.vue new file mode 100644 index 0000000..c59060c --- /dev/null +++ b/apps/machine/src/components/CardChip.vue @@ -0,0 +1,78 @@ + + + diff --git a/apps/machine/src/stores/atm.ts b/apps/machine/src/stores/atm.ts index 34acf7e..fd00df2 100644 --- a/apps/machine/src/stores/atm.ts +++ b/apps/machine/src/stores/atm.ts @@ -324,6 +324,9 @@ export const useAtmStore = defineStore('atm', () => { // hit-keyed withdraw/pay steps plus balance + fiat for display. Cleared when // the machine re-locks. Never logged. const loadedBoltCard = ref(null) + // The card balance is hidden by default on the public screen; the holder + // reveals it with the eye toggle. Resets on re-lock. + const cardBalanceRevealed = ref(false) const fiatCode = ref('USD') // Defaults are 0 — the operator's fee config (received via Nostr // kind-30078 `bitspire-fees:` envelope from satmachineadmin) @@ -459,6 +462,25 @@ export const useAtmStore = defineStore('atm', () => { // start), so App.vue's LockedView branch never renders on a non-access machine. const isLocked = computed(() => currentState.value === 'locked') + /** + * Fiat view of the loaded card's balance, the way the LNbits wallet page + * prices it: the card server's own currency and rate first (the wallet's + * currency, else the instance default); when it priced nothing, the ATM's + * fiat at its display rate. Null when neither is available. + */ + const loadedCardFiat = computed<{ amount: number; currency: string } | null>(() => { + const card = loadedBoltCard.value + if (!card) return null + if (card.currency && card.fiat !== null) return { amount: card.fiat, currency: card.currency } + if (btcPrice.value && btcPrice.value > 0) { + return { amount: (card.balanceSats / 1e8) * btcPrice.value, currency: fiatCode.value } + } + return null + }) + function toggleCardBalance() { + cardBalanceRevealed.value = !cardBalanceRevealed.value + } + const isCashIn = computed(() => { const state = snapshot.value?.value return typeof state === 'object' && 'cashIn' in state @@ -517,6 +539,7 @@ export const useAtmStore = defineStore('atm', () => { // re-locks) so the next customer starts fresh — never carry a card over. if (state === 'locked' && loadedBoltCard.value) { loadedBoltCard.value = null + cardBalanceRevealed.value = false } // Detect network from first invoice we see @@ -753,6 +776,7 @@ export const useAtmStore = defineStore('atm', () => { }) if (outcome.status === 'granted') { loadedBoltCard.value = opened.session + cardBalanceRevealed.value = false nfcStatus.value = { state: 'accepted', message: 'Card accepted' } grantAccess(outcome.role, outcome.credentialIdHash) } else { @@ -1869,6 +1893,9 @@ export const useAtmStore = defineStore('atm', () => { simulateBoltCardReceive, // Tap-to-enter: card session opened at the locked screen, reused at Complete loadedBoltCard, + cardBalanceRevealed, + loadedCardFiat, + toggleCardBalance, completeWithCard, simulateBoltCardEntry, diff --git a/apps/machine/src/views/CashInView.vue b/apps/machine/src/views/CashInView.vue index b75a2a4..a86acd0 100644 --- a/apps/machine/src/views/CashInView.vue +++ b/apps/machine/src/views/CashInView.vue @@ -3,6 +3,7 @@ import { watch, computed, ref } from 'vue' import { useRouter } from 'vue-router' import { useAtmStore } from '@/stores/atm' import { Button } from '@/components/ui/button' +import CardChip from '@/components/CardChip.vue' import { Alert, AlertDescription } from '@/components/ui/alert' import { Input } from '@/components/ui/input' import QRCode from '@/components/QRCode.vue' @@ -368,9 +369,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice) v-if="atmStore.loadedBoltCard" class="flex w-full max-w-md flex-col items-center gap-2 pt-2" > -

- Card ••{{ atmStore.loadedBoltCard.externalId.slice(-4) }} -

+