feat(machine): omnipresent light/dark toggle and dynamic network badge
Move light/dark toggle with Lucide Sun/Moon icons from IdleView to App.vue so it's visible on all screens. Detect Bitcoin network (mainnet/testnet/regtest) from BOLT-11 invoice prefix and persist in localStorage. Hide network badge on mainnet (implied). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a78fffb65
commit
73feb2b0d2
2 changed files with 44 additions and 10 deletions
|
|
@ -2,13 +2,14 @@
|
||||||
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
||||||
import { useAtmStore } from '@/stores/atm'
|
import { useAtmStore } from '@/stores/atm'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { Sun, Moon } from 'lucide-vue-next'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
const atmStore = useAtmStore()
|
const atmStore = useAtmStore()
|
||||||
const { current: currentTheme, themes, colorMode } = useTheme()
|
const { current: currentTheme, themes, colorMode } = useTheme()
|
||||||
const debugExpanded = ref(false)
|
const debugExpanded = ref(false)
|
||||||
const network = import.meta.env.VITE_NETWORK || 'regtest'
|
// Network detected dynamically from Lightning invoice prefix
|
||||||
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
|
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
|
||||||
|
|
||||||
function formatSats(sats: number): string {
|
function formatSats(sats: number): string {
|
||||||
|
|
@ -76,8 +77,8 @@ function toggleLiveServices() {
|
||||||
<line x1="12" y1="9" x2="12" y2="13" />
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
<h1 class="text-3xl font-bold">ATM Unavailable</h1>
|
<h1 class="text-[3.5rem] font-bold">ATM Unavailable</h1>
|
||||||
<p class="max-w-md text-center text-lg text-muted-foreground">
|
<p class="max-w-md text-center text-2xl text-muted-foreground">
|
||||||
This machine is temporarily out of service. Please try again later or use another machine.
|
This machine is temporarily out of service. Please try again later or use another machine.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="atmStore.debugMode" class="max-w-lg text-center font-mono text-sm text-destructive">
|
<p v-if="atmStore.debugMode" class="max-w-lg text-center font-mono text-sm text-destructive">
|
||||||
|
|
@ -89,18 +90,16 @@ function toggleLiveServices() {
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
<!-- Connection status + balance (top right) -->
|
<!-- Connection status + balance (top right) -->
|
||||||
<div
|
<div class="fixed right-6 top-6 z-50 flex flex-wrap items-center justify-end gap-2">
|
||||||
class="fixed right-2 top-2 z-50 flex flex-wrap items-center justify-end gap-1 sm:right-4 sm:top-4 sm:gap-2"
|
|
||||||
>
|
|
||||||
<Badge
|
<Badge
|
||||||
v-if="formattedBtcPrice"
|
v-if="formattedBtcPrice"
|
||||||
class="bg-primary/10 font-mono text-primary border border-primary/30 text-xs sm:text-sm"
|
class="bg-primary/10 font-mono text-primary border border-primary/30 text-base"
|
||||||
>
|
>
|
||||||
{{ formattedBtcPrice }}
|
{{ formattedBtcPrice }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="atmStore.balanceSats !== null"
|
v-if="atmStore.balanceSats !== null"
|
||||||
class="bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-xs sm:text-sm"
|
class="bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-base"
|
||||||
>
|
>
|
||||||
bal: {{ formatSats(atmStore.balanceSats) }} sats
|
bal: {{ formatSats(atmStore.balanceSats) }} sats
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -121,13 +120,25 @@ function toggleLiveServices() {
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else class="bg-muted text-muted-foreground">Mock</Badge>
|
<Badge v-else class="bg-muted text-muted-foreground">Mock</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="network !== 'mainnet'"
|
v-if="atmStore.network && atmStore.network !== 'mainnet'"
|
||||||
class="bg-warning/20 text-warning border border-warning/30"
|
class="bg-warning/20 text-warning border border-warning/30"
|
||||||
>
|
>
|
||||||
{{ network }}
|
{{ atmStore.network }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Light/dark toggle (always visible) -->
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="kiosk"
|
||||||
|
class="fixed bottom-6 right-6 z-50 gap-3"
|
||||||
|
@click="colorMode = colorMode === 'dark' ? 'light' : 'dark'"
|
||||||
|
>
|
||||||
|
<Sun v-if="colorMode === 'dark'" :size="28" />
|
||||||
|
<Moon v-else :size="28" />
|
||||||
|
{{ colorMode === 'dark' ? 'Light' : 'Dark' }}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<!-- Debug overlay (dev only) -->
|
<!-- Debug overlay (dev only) -->
|
||||||
<div
|
<div
|
||||||
v-if="atmStore.debugMode"
|
v-if="atmStore.debugMode"
|
||||||
|
|
|
||||||
|
|
@ -151,12 +151,28 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
const balanceSats = ref<number | null>(null)
|
const balanceSats = ref<number | null>(null)
|
||||||
const btcPrice = ref<number | null>(null) // local fiat per BTC (for display)
|
const btcPrice = ref<number | null>(null) // local fiat per BTC (for display)
|
||||||
const btcPriceUsd = ref<number | null>(null) // USD per BTC (always shown)
|
const btcPriceUsd = ref<number | null>(null) // USD per BTC (always shown)
|
||||||
|
const network = ref<'mainnet' | 'testnet' | 'regtest' | null>(
|
||||||
|
(localStorage.getItem('atm-network') as 'mainnet' | 'testnet' | 'regtest') || null
|
||||||
|
)
|
||||||
let stopBalanceWatch: (() => void) | null = null
|
let stopBalanceWatch: (() => void) | null = null
|
||||||
let pricePollingInterval: ReturnType<typeof setInterval> | null = null
|
let pricePollingInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
// Store reference to ATM services for direct calls
|
// Store reference to ATM services for direct calls
|
||||||
let atmServicesRef: ATMServices | null = null
|
let atmServicesRef: ATMServices | null = null
|
||||||
|
|
||||||
|
/** Detect Bitcoin network from a BOLT-11 invoice prefix (called once, persisted) */
|
||||||
|
function detectNetworkFromInvoice(invoice: string) {
|
||||||
|
if (network.value) return // already detected
|
||||||
|
const lower = invoice.toLowerCase()
|
||||||
|
if (lower.startsWith('lnbcrt')) network.value = 'regtest'
|
||||||
|
else if (lower.startsWith('lntb')) network.value = 'testnet'
|
||||||
|
else if (lower.startsWith('lnbc')) network.value = 'mainnet'
|
||||||
|
if (network.value) {
|
||||||
|
localStorage.setItem('atm-network', network.value)
|
||||||
|
console.log('[ATM] Detected network:', network.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const currentState = computed(() => {
|
const currentState = computed(() => {
|
||||||
if (!snapshot.value) return 'uninitialized'
|
if (!snapshot.value) return 'uninitialized'
|
||||||
|
|
@ -220,6 +236,11 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
if ('cashOut' in state) currentNested = state.cashOut as string
|
if ('cashOut' in state) currentNested = state.cashOut as string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect network from first invoice we see
|
||||||
|
if (newSnapshot.context.invoice) {
|
||||||
|
detectNetworkFromInvoice(newSnapshot.context.invoice)
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-advance past waitingForCashTaken when HAL is connected,
|
// Auto-advance past waitingForCashTaken when HAL is connected,
|
||||||
// because HAL's dispenseCash already calls waitForBillsRemoved()
|
// because HAL's dispenseCash already calls waitForBillsRemoved()
|
||||||
if (
|
if (
|
||||||
|
|
@ -394,6 +415,7 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
paymentError.value = null
|
paymentError.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
detectNetworkFromInvoice(invoice)
|
||||||
console.log(
|
console.log(
|
||||||
'[ATM] Paying invoice:',
|
'[ATM] Paying invoice:',
|
||||||
invoice.slice(0, 40) + '...',
|
invoice.slice(0, 40) + '...',
|
||||||
|
|
@ -999,6 +1021,7 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
balanceSats,
|
balanceSats,
|
||||||
btcPrice,
|
btcPrice,
|
||||||
btcPriceUsd,
|
btcPriceUsd,
|
||||||
|
network,
|
||||||
halServices,
|
halServices,
|
||||||
isPayingInvoice,
|
isPayingInvoice,
|
||||||
isRequestingDebit,
|
isRequestingDebit,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue