diff --git a/package.json b/package.json index efadb48..8d056bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.3.1", + "version": "1.3.2", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/src/hooks/useCitrine.ts b/src/hooks/useCitrine.ts index 44e54e1..aa3ada4 100644 --- a/src/hooks/useCitrine.ts +++ b/src/hooks/useCitrine.ts @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { useAppContext } from '@/hooks/useAppContext'; import { CITRINE_RELAY_URL, + citrinePreview, isCitrinePossible, probeCitrine, } from '@/lib/citrine'; @@ -46,6 +47,10 @@ export function useCitrine() { const active = possible && enabled && reachable; + // DEV PREVIEW ONLY: a forced visual state for browser design review. Purely + // cosmetic — it never affects `active`/`url`, so no traffic is ever rerouted. + const preview = citrinePreview(); + const setEnabled = useCallback( (next: boolean) => { updateConfig((current) => ({ ...current, useCitrineRelay: next })); @@ -67,6 +72,12 @@ export function useCitrine() { */ settled: isFetched, url: active ? CITRINE_RELAY_URL : null, + /** + * DEV PREVIEW ONLY — a forced UI state ('off'|'searching'|'live'|'waiting') + * when the citrine:preview flag is set, else null. Cosmetic; does not affect + * `active`/`url`. The Settings card uses it to override its computed state. + */ + preview, setEnabled, /** Re-run the reachability probe (e.g. after the user starts Citrine). */ recheck: refetch, diff --git a/src/lib/citrine.ts b/src/lib/citrine.ts index 2b5f9c1..0c54f7e 100644 --- a/src/lib/citrine.ts +++ b/src/lib/citrine.ts @@ -25,6 +25,45 @@ /** Citrine's default localhost relay endpoint. */ export const CITRINE_RELAY_URL = 'ws://127.0.0.1:4869'; +/** Forcible UI states for the dev preview flag. */ +export type CitrinePreviewState = 'off' | 'searching' | 'live' | 'waiting'; +const PREVIEW_KEY = 'citrine:preview'; + +/** + * DEV PREVIEW ONLY — lets the (normally Android-only) Citrine UI render in a + * browser so the design can be reviewed without an APK build. + * + * Set in the browser console: + * localStorage.setItem('citrine:preview', '1') // show the card, real behaviour + * localStorage.setItem('citrine:preview', 'live') // force the "connected" state + * localStorage.setItem('citrine:preview', 'waiting') // force "not detected" + * localStorage.setItem('citrine:preview', 'searching')// force "searching…" + * localStorage.removeItem('citrine:preview') // back to normal + * + * CRITICAL: this unlocks RENDERING ONLY. It never makes the local relay + * `active` and never yields a relay `url`, so no query or publish is ever routed + * anywhere different — production relay behaviour is completely unchanged. A + * forced state is purely cosmetic. + */ +export function citrinePreview(): CitrinePreviewState | null { + try { + const raw = (globalThis as { localStorage?: Storage }).localStorage?.getItem(PREVIEW_KEY); + if (!raw) return null; + if (raw === 'off' || raw === 'searching' || raw === 'live' || raw === 'waiting') { + return raw; + } + // Any other truthy value ("1"/"true") = show the card with real behaviour. + return 'off'; + } catch { + return null; + } +} + +/** True if the dev preview flag is set at all (any value). */ +export function isCitrinePreview(): boolean { + return citrinePreview() !== null; +} + /** * True when running inside a Capacitor native Android shell. Citrine is an * Android app, so the local relay can only exist there. This never throws in a @@ -39,13 +78,13 @@ export function isAndroidNative(): boolean { } /** - * True if the Citrine relay URL *could* be reachable on this device — i.e. we're - * on native Android. This is a cheap synchronous gate used to decide whether to - * even offer the feature / attempt a probe. Actual reachability is confirmed by - * probeCitrine(). + * True if the Citrine UI/feature should be offered on this device — native + * Android, OR the dev preview flag is set (browser design review). Actual + * reachability is still confirmed separately by probeCitrine(), which fails + * cleanly in a browser, so preview never routes real traffic. */ export function isCitrinePossible(): boolean { - return isAndroidNative(); + return isAndroidNative() || isCitrinePreview(); } /** diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 42bb4bd..ebf9f93 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -283,7 +283,9 @@ function CitrineCard() { // Not on native Android → the local relay can't exist. Hide entirely. if (!citrine.possible) return null; - const state: 'off' | 'searching' | 'live' | 'waiting' = !citrine.enabled + // Normal computed state. In dev preview (citrine.preview set), a forced state + // overrides it purely for visual review — real relay behaviour is unaffected. + const computedState: 'off' | 'searching' | 'live' | 'waiting' = !citrine.enabled ? 'off' : citrine.reachable ? 'live' @@ -291,6 +293,13 @@ function CitrineCard() { ? 'searching' : 'waiting'; + const state = citrine.preview ?? computedState; + // Treat any non-off forced preview state as "enabled" so the expanded detail + // (chips / CTA / status) renders for review. + const enabled = citrine.preview ? citrine.preview !== 'off' : citrine.enabled; + // Spinner reflects a real probe, or a forced "searching" preview state. + const probing = citrine.probing || state === 'searching'; + const handleToggle = () => { const next = !citrine.enabled; citrine.setEnabled(next); @@ -339,7 +348,7 @@ function CitrineCard() { )} /> - {citrine.enabled && ( + {enabled && ( {/* Expanding detail — only when enabled */} - {citrine.enabled && ( + {enabled && (
{/* Status row + recheck */}
@@ -430,14 +439,14 @@ function CitrineCard() {