forked from
did:plc:2hcnfmbfr4ucfbjpnvjqvt3e/bbell
wip bsky client for the web & android
1<script setup lang="ts">
2import { ref, onMounted, watch } from 'vue'
3import { App, type URLOpenListenerEvent } from '@capacitor/app'
4
5import { useNavigationStore } from '@/stores/navigation'
6import { useEnvironmentStore } from '@/stores/environment'
7import { useThemeStore } from '@/stores/theme'
8import { useAuthStore } from '@/stores/auth'
9import { useModalStore } from '@/stores/modal'
10
11import OAuthCallback from '@/views/Auth/OAuthCallback.vue'
12import OnboardingFlow from '@/views/Onboarding/OnboardingFlow.vue'
13
14import AppShell from '@/components/Layout/AppShell.vue'
15import SplashScreen from '@/components/Layout/SplashScreen.vue'
16import ModalStack from '@/components/UI/ModalStack.vue'
17import PronounsModal from '@/components/Modals/PronounsModal.vue'
18
19import KEYS from './utils/keys'
20
21type AppPhase = 'loading' | 'callback' | 'intro' | 'shell'
22const currentPhase = ref<AppPhase>('loading')
23
24const nav = useNavigationStore()
25const env = useEnvironmentStore()
26const theme = useThemeStore()
27const auth = useAuthStore()
28const modals = useModalStore()
29
30// init stuff
31// ========================================================
32async function initializeApp() {
33 theme.init()
34 env.init()
35 auth.init()
36
37 const path = window.location.pathname
38 if (path.includes('/oauth/callback')) {
39 currentPhase.value = 'callback'
40 return
41 }
42
43 const wait = () => new Promise((resolve) => setTimeout(resolve, 1500))
44
45 // waiting for auth
46 // we then either determine the Next Phase - either the onboarding flow or to the shell
47 if (auth.isLoading) {
48 const unwatch = watch(
49 () => auth.isLoading,
50 async (loading) => {
51 if (!loading) {
52 await wait()
53 unwatch()
54 determineNextPhase()
55 }
56 },
57 )
58 } else {
59 await wait()
60 determineNextPhase()
61 }
62}
63
64function determineNextPhase() {
65 const hasSeenIntro = localStorage.getItem(KEYS.STATE.INTRO_COMPLETE) === 'true'
66
67 // onboarding flow!
68 if (!hasSeenIntro) {
69 currentPhase.value = 'intro'
70 }
71 // shell/main app!
72 else {
73 finishStartup()
74 }
75}
76
77function finishStartup() {
78 nav.init()
79 currentPhase.value = 'shell'
80
81 const profile = auth.profile
82 if (!profile) return
83
84 if (!profile?.pronouns) {
85 setTimeout(() => {
86 const dismissed = localStorage.getItem(KEYS.STATE.WOKE_DISMISSED)
87 if (!dismissed) {
88 modals.open(PronounsModal)
89 }
90 }, 750)
91 }
92}
93
94// event handlers
95// ========================================================
96const onAuthCallbackComplete = () => {
97 window.history.replaceState(null, '', '/')
98 theme.init()
99 env.init()
100 finishStartup()
101}
102
103const onIntroComplete = (action: 'stay' | 'login') => {
104 localStorage.setItem(KEYS.STATE.INTRO_COMPLETE, 'true')
105 finishStartup()
106
107 if (action === 'login') {
108 setTimeout(() => {
109 nav.push('login')
110 }, 100)
111 }
112}
113
114// ========================================================
115onMounted(() => {
116 initializeApp()
117
118 App.addListener('appUrlOpen', function (event: URLOpenListenerEvent) {
119 const url = new URL(event.url)
120 const path = url.pathname
121 const hash = url.hash
122
123 if (!path) return
124
125 if (path.startsWith('/oauth/callback')) {
126 auth._hash = hash
127 currentPhase.value = 'callback'
128 } else {
129 nav.navigateToUrl(path)
130 }
131 })
132})
133</script>
134
135<template>
136 <div class="app-root">
137 <ModalStack />
138
139 <Transition name="fade" mode="out-in">
140 <SplashScreen v-if="currentPhase === 'loading'" key="loading" />
141
142 <OAuthCallback
143 v-else-if="currentPhase === 'callback'"
144 key="callback"
145 class="view-layer"
146 @complete="onAuthCallbackComplete"
147 />
148
149 <OnboardingFlow
150 v-else-if="currentPhase === 'intro'"
151 key="intro"
152 @complete="onIntroComplete('stay')"
153 />
154
155 <div
156 v-else-if="currentPhase === 'shell'"
157 key="shell"
158 class="shell-wrapper"
159 :inert="modals.stack.length > 0"
160 >
161 <AppShell />
162 </div>
163 </Transition>
164 </div>
165</template>
166
167<style scoped>
168.app-root {
169 width: 100%;
170 height: 100%;
171}
172
173.view-layer {
174 position: absolute;
175 inset: 0;
176 width: 100vw;
177 height: 100vh;
178}
179
180.shell-wrapper {
181 height: 100%;
182 width: 100%;
183}
184
185.fade-enter-active,
186.fade-leave-active {
187 transition: opacity 0.3s ease;
188}
189
190.fade-enter-from,
191.fade-leave-to {
192 opacity: 0;
193}
194</style>