fix(stage-*): onboarding issues

This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent 302c5cd618
commit e30eba8c71
10 changed files with 69 additions and 74 deletions
@@ -1,17 +1,14 @@
<script setup lang="ts">
import { listSessions, signOut } from '@proj-airi/stage-ui/libs/auth'
import { signOut } from '@proj-airi/stage-ui/libs/auth'
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
import { onClickOutside, useMediaQuery } from '@vueuse/core'
import { onClickOutside } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { toast } from 'vue-sonner'
const authStore = useAuthStore()
const { isAuthenticated, user, credits } = storeToRefs(authStore)
const isMobile = useMediaQuery('(max-width: 768px)')
const userName = computed(() => user.value?.name)
const userAvatar = computed(() => user.value?.image)
const showDropdown = ref(false)
@@ -20,22 +17,6 @@ const dropdownRef = ref(null)
onClickOutside(dropdownRef, () => {
showDropdown.value = false
})
function handleLogout() {
signOut()
}
async function handleListSessions() {
try {
const { data: sessions } = await listSessions()
if (sessions) {
toast.success(`You have ${sessions.length} active sessions.`)
}
}
catch (error) {
toast.error(error instanceof Error ? error.message : 'An unknown error occurred')
}
}
</script>
<template>
@@ -53,29 +34,16 @@ async function handleListSessions() {
<div i-solar:settings-minimalistic-bold-duotone size-5 text="neutral-500 dark:neutral-400" />
</RouterLink>
<template v-if="isMobile">
<button
border="2 solid neutral-100/60 dark:neutral-800/30"
bg="neutral-50/70 dark:neutral-800/70"
w-fit flex items-center justify-center rounded-xl p-2 backdrop-blur-md
title="Login"
type="button"
@click="authStore.isLoginDrawerOpen = true"
>
<div i-solar:user-bold-duotone />
</button>
</template>
<template v-else>
<RouterLink
border="2 solid neutral-100/60 dark:neutral-800/30"
bg="neutral-50/70 dark:neutral-800/70"
w-fit flex items-center justify-center rounded-xl p-2 backdrop-blur-md
:title="isAuthenticated ? `Logged in as ${userName}` : 'Login'"
to="/auth/login"
>
<div i-solar:user-bold-duotone />
</RouterLink>
</template>
<button
border="2 solid neutral-100/60 dark:neutral-800/30"
bg="neutral-50/70 dark:neutral-800/70"
w-fit flex items-center justify-center rounded-xl p-2 backdrop-blur-md
title="Login"
type="button"
@click="authStore.needsLogin = true"
>
<div i-solar:user-bold-duotone />
</button>
</template>
<!-- Authenticated: Avatar Dropdown -->
@@ -135,14 +103,6 @@ async function handleListSessions() {
</div>
<div class="py-1">
<button
class="group w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-neutral-700 transition hover:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-800"
@click="handleListSessions"
>
<div class="i-solar:devices-bold-duotone text-lg text-neutral-400 transition group-hover:text-primary-500" />
Active Sessions
</button>
<RouterLink
to="/settings/flux"
class="group w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-neutral-700 transition hover:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-800"
@@ -165,7 +125,7 @@ async function handleListSessions() {
<div class="py-1">
<button
class="group w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-red-600 transition hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
@click="handleLogout"
@click="signOut"
>
<div class="i-solar:logout-3-bold-duotone text-lg transition group-hover:text-red-600 dark:group-hover:text-red-400" />
Logout
+10 -3
View File
@@ -1,16 +1,23 @@
<script setup lang="ts">
import { LoginDrawer } from '@proj-airi/stage-ui/components/auth/index'
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
import { useMediaQuery } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { RouterView } from 'vue-router'
const authStore = useAuthStore()
const { isLoginDrawerOpen } = storeToRefs(authStore)
const isMobile = useMediaQuery('(max-width: 768px)')
const { needsLogin } = storeToRefs(useAuthStore())
</script>
<template>
<main h-full font-cute>
<RouterView />
<LoginDrawer v-model:open="isLoginDrawerOpen" />
<Teleport to="body">
<LoginDrawer
v-if="isMobile"
v-model:open="needsLogin"
/>
</Teleport>
</main>
</template>
@@ -14,13 +14,13 @@ const router = useRouter()
const { t } = useI18n()
const authStore = useAuthStore()
const providersStore = useProvidersStore()
const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore)
const { isAuthenticated, credits, needsLogin } = storeToRefs(authStore)
const providerId = 'official-provider'
const providerMetadata = providersStore.getProviderMetadata(providerId)
function handleLogin() {
isLoginDrawerOpen.value = true
needsLogin.value = true
}
</script>
@@ -14,13 +14,13 @@ const router = useRouter()
const { t } = useI18n()
const authStore = useAuthStore()
const providersStore = useProvidersStore()
const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore)
const { isAuthenticated, credits, needsLogin } = storeToRefs(authStore)
const providerId = 'official-provider-speech'
const providerMetadata = providersStore.getProviderMetadata(providerId)
function handleLogin() {
isLoginDrawerOpen.value = true
needsLogin.value = true
}
</script>
@@ -14,13 +14,13 @@ const router = useRouter()
const { t } = useI18n()
const authStore = useAuthStore()
const providersStore = useProvidersStore()
const { isAuthenticated, credits, isLoginDrawerOpen } = storeToRefs(authStore)
const { isAuthenticated, credits, needsLogin } = storeToRefs(authStore)
const providerId = 'official-provider-transcription'
const providerMetadata = providersStore.getProviderMetadata(providerId)
function handleLogin() {
isLoginDrawerOpen.value = true
needsLogin.value = true
}
</script>
@@ -65,7 +65,7 @@ async function handleSignIn(provider: OAuthProvider) {
</Button>
</div>
<div class="mt-10 pb-2 text-center text-xs text-gray-400">
By continuing, you agree to our <a href="#" class="underline">Terms</a> and <a href="#" class="underline">Privacy Policy</a>.
By continuing, you agree to our <a href="https://airi.moeru.ai/docs/en/about/terms" class="underline">Terms</a> and <a href="https://airi.moeru.ai/docs/en/about/privacy" class="underline">Privacy Policy</a>.
</div>
</div>
</DrawerContent>
@@ -24,8 +24,8 @@ const languages = computed(() => {
})
function handleLogin() {
onboardingStore.shouldShowSetup = false
authStore.isLoginOpen = true
onboardingStore.showingSetup = false
authStore.needsLogin = true
}
function handleLocalSetup() {
@@ -82,7 +82,7 @@ function handleLocalSetup() {
/>
</div>
</div>
<div flex="~ row gap-3">
<div flex="~ gap-3" class="flex-col md:flex-row">
<Button
v-motion
:initial="{ opacity: 0 }"
+2 -2
View File
@@ -40,8 +40,8 @@ export async function signOut() {
await authClient.signOut()
const authStore = useAuthStore()
authStore.user = undefined
authStore.session = undefined
authStore.user = null
authStore.session = null
}
export async function signIn(provider: OAuthProvider) {
+25 -5
View File
@@ -1,5 +1,6 @@
import type { Session, User } from 'better-auth'
import { StorageSerializers, useLocalStorage, useMediaQuery, whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
@@ -12,14 +13,31 @@ import { client } from '../composables/api'
* `providers` to safely depend on it without creating a circular import.
*/
export const useAuthStore = defineStore('auth', () => {
const user = ref<User>()
const session = ref<Session>()
const user = useLocalStorage<User | null>('auth/v1/user', null, {
// Why: https://github.com/vueuse/vueuse/pull/614#issuecomment-875450160
serializer: StorageSerializers.object,
})
const session = useLocalStorage<Session | null>('auth/v1/session', null, { serializer: StorageSerializers.object })
const isAuthenticated = computed(() => !!user.value && !!session.value)
const userId = computed(() => user.value?.id ?? 'local')
const credits = ref<number>(0)
const credits = useLocalStorage<number>('user/v1/flux', 0)
const isLoginOpen = ref(false)
// For controlling the login drawer on mobile
const needsLogin = ref(false)
const isMobile = useMediaQuery('(max-width: 768px)')
whenever(needsLogin, () => {
if (isMobile.value) {
return
}
// TODO: type safe, import `useRouter` from router.ts
window.location.href = '/auth/login'
})
// Reset status when changing the window viewport
watch(isMobile, () => needsLogin.value = false)
// --- Lifecycle hooks ---
type AuthHook = () => void | Promise<void>
@@ -78,6 +96,8 @@ export const useAuthStore = defineStore('auth', () => {
watch(isAuthenticated, async (val) => {
if (val) {
updateCredits()
needsLogin.value = false
}
else {
credits.value = 0
@@ -91,7 +111,7 @@ export const useAuthStore = defineStore('auth', () => {
isAuthenticated,
credits,
updateCredits,
isLoginDrawerOpen,
needsLogin,
onAuthenticated,
onLogout,
}
+9 -1
View File
@@ -2,6 +2,7 @@ import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useAuthStore } from './auth'
import { useProvidersStore } from './providers'
const essentialProviderIds = ['openai', 'azure-openai', 'anthropic', 'google-generative-ai', 'openrouter-ai', 'ollama', 'deepseek', 'openai-compatible'] as const
@@ -13,6 +14,7 @@ function hasNonEmptyText(value: unknown): boolean {
export const useOnboardingStore = defineStore('onboarding', () => {
const providersStore = useProvidersStore()
const authStore = useAuthStore()
// Track if first-time setup has been completed or skipped
const hasCompletedSetup = useLocalStorage('onboarding/completed', false)
@@ -37,7 +39,13 @@ export const useOnboardingStore = defineStore('onboarding', () => {
})
// Check if first-time setup should be shown
const needsOnboarding = computed(() => !hasSkippedSetup.value && !hasCompletedSetup.value)
const skipOnboardingPath = ['/auth/login']
const needsOnboarding = computed(() =>
!authStore.isAuthenticated
&& !hasSkippedSetup.value
&& !hasCompletedSetup.value
&& !skipOnboardingPath.includes(document.location.pathname),
)
// Keep in-memory display flag aligned with persisted onboarding status
// when setup is completed/skipped from another window (desktop multi-window case).