fix(demo): companion app skips the demo intro — lands straight on /login
When the demo build runs inside the Android companion WebView (window.ArchipelagoNative bridge, detected via the existing isCompanionApp()), all four IS_DEMO intro branch sites now skip the typing splash and /onboarding/intro and route directly to /login, as if the intro was already seen: - App.vue root-boot replay request (companion never requests the splash) - App.vue post-splash demo routing - RootRedirect proceedToApp() and the server-up onMounted demo branch The skip paths write nothing to localStorage/sessionStorage (RootRedirect skips even its boot log() there), so the browser/PWA demo intro — which replays on every fresh root boot — is byte-identical to before, and non-demo builds short-circuit on IS_DEMO before isCompanionApp() runs. Adds a small isCompanionApp() bridge-detection unit test (700 tests green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b80e7c3487
commit
d54517cf0b
@ -110,6 +110,7 @@ import { useScreensaverStore } from '@/stores/screensaver'
|
||||
import { useUIModeStore } from '@/stores/uiMode'
|
||||
import { startRemoteRelay, stopRemoteRelay } from '@/api/remote-relay'
|
||||
import { shouldShowIntroSplash } from '@/utils/introSplash'
|
||||
import { isCompanionApp } from '@/utils/openExternal'
|
||||
|
||||
const router = useRouter()
|
||||
const screensaverStore = useScreensaverStore()
|
||||
@ -430,7 +431,8 @@ onMounted(async () => {
|
||||
// refresh) starts with the typing splash for the full effect. In-session SPA
|
||||
// navigation never remounts App, and deep-route refreshes keep their place.
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO && bootPath === '/') replayRequested = true
|
||||
// Companion in-app demo never requests the splash replay; browser demo unaffected.
|
||||
if (IS_DEMO && bootPath === '/' && !isCompanionApp()) replayRequested = true
|
||||
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
||||
// Root boots always ask the backend — even when this browser thinks it has
|
||||
// seen the intro. Both `neode_intro_seen` and `neode_onboarding_complete`
|
||||
@ -586,6 +588,12 @@ async function handleSplashComplete() {
|
||||
{
|
||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||
if (IS_DEMO) {
|
||||
// Companion in-app demo skips the intro and lands on /login; browser demo unaffected.
|
||||
if (isCompanionApp()) {
|
||||
await router.push('/login').catch(() => {})
|
||||
reveal()
|
||||
return
|
||||
}
|
||||
await router.push('/onboarding/intro').catch(() => {})
|
||||
reveal()
|
||||
return
|
||||
|
||||
29
neode-ui/src/utils/__tests__/openExternal.test.ts
Normal file
29
neode-ui/src/utils/__tests__/openExternal.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { isCompanionApp } from '../openExternal'
|
||||
|
||||
// isCompanionApp() is the single companion-detection source used to skip the
|
||||
// demo intro (App.vue + RootRedirect.vue) and by appLauncher — it must be true
|
||||
// iff the native shell injected window.ArchipelagoNative with openInApp.
|
||||
|
||||
type TestWindow = Window & { ArchipelagoNative?: unknown }
|
||||
const w = window as TestWindow
|
||||
|
||||
afterEach(() => {
|
||||
delete w.ArchipelagoNative
|
||||
})
|
||||
|
||||
describe('isCompanionApp', () => {
|
||||
it('is false in a plain browser/PWA (no bridge injected)', () => {
|
||||
expect(isCompanionApp()).toBe(false)
|
||||
})
|
||||
|
||||
it('is true when the native shell injected the bridge with openInApp', () => {
|
||||
w.ArchipelagoNative = { openInApp: () => {} }
|
||||
expect(isCompanionApp()).toBe(true)
|
||||
})
|
||||
|
||||
it('is false when the bridge exists but lacks a callable openInApp', () => {
|
||||
w.ArchipelagoNative = { openExternal: () => {} }
|
||||
expect(isCompanionApp()).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -17,6 +17,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isOnboardingComplete } from '@/composables/useOnboarding'
|
||||
import { IS_DEMO } from '@/composables/useDemoIntro'
|
||||
import { isCompanionApp } from '@/utils/openExternal'
|
||||
import BootScreen from '@/components/BootScreen.vue'
|
||||
|
||||
const router = useRouter()
|
||||
@ -80,6 +81,13 @@ async function checkOnboarded(): Promise<boolean> {
|
||||
|
||||
async function proceedToApp() {
|
||||
if (IS_DEMO) {
|
||||
// Companion in-app demo skips the intro (acts as intro-already-seen) and
|
||||
// lands on /login; browser demo unaffected. No log() here — it writes
|
||||
// sessionStorage, and the skip path must write nothing.
|
||||
if (isCompanionApp()) {
|
||||
router.replace('/login').catch(() => {})
|
||||
return
|
||||
}
|
||||
demoRoute()
|
||||
return
|
||||
}
|
||||
@ -147,6 +155,12 @@ onMounted(async () => {
|
||||
if (isUp) {
|
||||
// Demo: per-day intro gate instead of server-side onboarding state.
|
||||
if (IS_DEMO) {
|
||||
// Companion in-app demo skips the intro; browser demo unaffected.
|
||||
// No log() — the skip path must write nothing to storage.
|
||||
if (isCompanionApp()) {
|
||||
router.replace('/login').catch(() => {})
|
||||
return
|
||||
}
|
||||
demoRoute()
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user