fix(stage-*): onboarding issues
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
|
||||
+2
-2
@@ -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 }"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user