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 @@ + + + + + + + + {{ label }} + + + {{ sats }} sats + ≈ {{ fiat }} + + + •••••• + + + + + + + + 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) }} - + - - Card ••{{ atmStore.loadedBoltCard.externalId.slice(-4) }} - + + +
- Card ••{{ atmStore.loadedBoltCard.externalId.slice(-4) }} -