fix(intro): hold companion popup until dashboard reveal cinematic ends
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s

The remote-companion popup fired on a blind 5s timer while the
first-visit dashboard entrance cinematic runs for 8s, so on a fresh
install it appeared mid-reveal and broke the intro. The cinematic now
publishes an introCinematicPlaying flag via the loginTransition store,
and the popup waits for it to clear (plus a 2s grace) before showing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-16 06:57:42 -04:00
parent e9c3311eff
commit fa91faa67c
3 changed files with 46 additions and 3 deletions

View File

@ -78,6 +78,7 @@
import { ref, onMounted, watch } from 'vue'
import * as QRCode from 'qrcode'
import { IS_DEMO } from '@/composables/useDemoIntro'
import { useLoginTransitionStore } from '@/stores/loginTransition'
const STORAGE_KEY = 'neode_companion_intro_seen'
// Absolute URL so the QR works when scanned by a phone (a relative path has no
@ -93,17 +94,40 @@ const visible = ref(false)
const qrDataUrl = ref('')
const companionDownloadUrl = import.meta.env.VITE_COMPANION_APK_URL || DEFAULT_DOWNLOAD_URL
const loginTransition = useLoginTransitionStore()
// Base delay before the popup may appear, and extra breathing room after the
// dashboard entrance cinematic ends so the popup never cuts into the reveal.
const BASE_DELAY_MS = 5000
const POST_INTRO_GRACE_MS = 2000
onMounted(() => {
try {
if (localStorage.getItem(STORAGE_KEY) !== '1') {
// Delay slightly so it doesn't compete with login animation
setTimeout(() => { visible.value = true }, 5000)
setTimeout(maybeShow, BASE_DELAY_MS)
}
} catch {
// localStorage unavailable
}
})
function maybeShow() {
if (loginTransition.introCinematicPlaying) {
// First-visit reveal is still running hold until it finishes.
const stop = watch(
() => loginTransition.introCinematicPlaying,
(playing) => {
if (!playing) {
stop()
setTimeout(() => { visible.value = true }, POST_INTRO_GRACE_MS)
}
},
)
return
}
visible.value = true
}
watch(visible, async (isVisible) => {
if (!isVisible) return
qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, {

View File

@ -15,6 +15,12 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => {
const pendingWelcomeTyping = ref(false)
/** Trigger welcome typing on Home - set true after dashboard animation finishes */
const startWelcomeTyping = ref(false)
/**
* True while the dashboard entrance cinematic (zoom/glitch reveal) is
* playing. Overlays that would visually compete with it (e.g. the
* companion intro popup) hold off until this flips back to false.
*/
const introCinematicPlaying = ref(false)
function setJustLoggedIn(value: boolean) {
justLoggedIn.value = value
@ -32,6 +38,10 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => {
startWelcomeTyping.value = value
}
function setIntroCinematicPlaying(value: boolean) {
introCinematicPlaying.value = value
}
return {
justLoggedIn,
setJustLoggedIn,
@ -41,5 +51,7 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => {
setPendingWelcomeTyping,
startWelcomeTyping,
setStartWelcomeTyping,
introCinematicPlaying,
setIntroCinematicPlaying,
}
})

View File

@ -373,6 +373,7 @@ onMounted(() => {
// un-forced call would gate itself silent.
playDashboardLoadOomph(true)
showZoomIn.value = true
loginTransition.setIntroCinematicPlaying(true)
loginTransition.setPendingWelcomeTyping(true)
loginTransition.setJustCompletedOnboarding(false)
loginTransition.setJustLoggedIn(false)
@ -383,7 +384,10 @@ onMounted(() => {
scheduledTimeout(triggerRevealGlitch, 500)
scheduledTimeout(triggerRevealGlitch, 1200)
scheduledTimeout(triggerRevealGlitch, 2000)
scheduledTimeout(() => { showZoomIn.value = false }, 8000)
scheduledTimeout(() => {
showZoomIn.value = false
loginTransition.setIntroCinematicPlaying(false)
}, 8000)
scheduledTimeout(() => {
loginTransition.setStartWelcomeTyping(true)
loginTransition.setPendingWelcomeTyping(false)
@ -405,6 +409,9 @@ onMounted(() => {
onBeforeUnmount(() => {
document.body.classList.remove('dashboard-active')
// Timers are cleared below; make sure overlays waiting on the cinematic
// aren't left blocked forever if we unmount mid-reveal.
loginTransition.setIntroCinematicPlaying(false)
window.removeEventListener('keydown', handleKioskShortcuts)
for (const id of pendingTimers) clearTimeout(id)
pendingTimers.length = 0