feat(stage-pocket): request permissions onboarding (#1292)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
LemonNeko
2026-03-12 14:49:39 +08:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 41ce282e95
commit 589553c0e3
10 changed files with 155 additions and 57 deletions
@@ -12,7 +12,8 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.1.0"),
.package(name: "CapacitorLocalNotifications", path: "../../../../../node_modules/.pnpm/@capacitor+local-notifications@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/local-notifications")
.package(name: "CapacitorLocalNotifications", path: "../../../../../node_modules/.pnpm/@capacitor+local-notifications@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/local-notifications"),
.package(name: "CapacitorNativeSettings", path: "../../../../../node_modules/.pnpm/capacitor-native-settings@8.0.0_@capacitor+core@8.1.0/node_modules/capacitor-native-settings")
],
targets: [
.target(
@@ -20,7 +21,8 @@ let package = Package(
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications")
.product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"),
.product(name: "CapacitorNativeSettings", package: "CapacitorNativeSettings")
]
)
]
+1
View File
@@ -58,6 +58,7 @@
"@xsai/stream-transcription": "0.4.0-beta.8",
"@xsai/utils-chat": "catalog:",
"animejs": "^4.3.6",
"capacitor-native-settings": "catalog:",
"colorjs.io": "^0.6.1",
"culori": "^4.0.2",
"date-fns": "^4.1.0",
+15 -3
View File
@@ -16,6 +16,8 @@ import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { toast, Toaster } from 'vue-sonner'
import OnboardingPermissionsStep from './components/onboarding/step-permissions.vue'
const contextBridgeStore = useContextBridgeStore()
const i18n = useI18n()
const displayModelsStore = useDisplayModelsStore()
@@ -24,7 +26,7 @@ const settings = storeToRefs(settingsStore)
const onboardingStore = useOnboardingStore()
const serverChannelStore = useModsServerChannelStore()
const characterOrchestratorStore = useCharacterOrchestratorStore()
const { shouldShowSetup } = storeToRefs(onboardingStore)
const { showingSetup } = storeToRefs(onboardingStore)
const { isDark } = useTheme()
const cardStore = useAiriCardStore()
const analyticsStore = useSharedAnalyticsStore()
@@ -68,7 +70,9 @@ onMounted(async () => {
analyticsStore.initialize()
cardStore.initialize()
onboardingStore.initializeSetupCheck()
if (onboardingStore.needsOnboarding) {
onboardingStore.showingSetup = true
}
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
await contextBridgeStore.initialize()
@@ -90,6 +94,13 @@ function handleSetupConfigured() {
function handleSetupSkipped() {
onboardingStore.markSetupSkipped()
}
const extraSteps = computed(() => [
{
id: 'step-permissions',
component: OnboardingPermissionsStep,
},
])
</script>
<template>
@@ -115,7 +126,8 @@ function handleSetupSkipped() {
<!-- First Time Setup Dialog -->
<OnboardingDialog
v-model="shouldShowSetup"
v-model="showingSetup"
:extra-steps="extraSteps"
@configured="handleSetupConfigured"
@skipped="handleSetupSkipped"
/>
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { Capacitor } from '@capacitor/core'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Button } from '@proj-airi/ui'
import { AndroidSettings, IOSSettings, NativeSettings } from 'capacitor-native-settings'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
type PermissionState = boolean | undefined
interface Props {
onNext: () => Promise<void> | void
onPrevious: () => void
}
const props = defineProps<Props>()
const { t } = useI18n()
const isNativePlatform = Capacitor.isNativePlatform()
const notificationPermissionGranted = ref<PermissionState>(undefined)
async function requestNotificationPermission() {
const beforeRequest = await LocalNotifications.checkPermissions()
if (beforeRequest.display === 'granted') {
notificationPermissionGranted.value = true
return
}
const requested = await LocalNotifications.requestPermissions()
if (requested.display === 'granted') {
notificationPermissionGranted.value = true
return
}
if (isNativePlatform) {
NativeSettings.open({
optionAndroid: AndroidSettings.AppNotification,
optionIOS: IOSSettings.AppNotification,
})
}
}
</script>
<template>
<div h-full flex flex-col gap-4>
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
<button outline-none @click="props.onPrevious">
<div i-solar:alt-arrow-left-line-duotone h-5 w-5 />
</button>
<h2 class="flex-1 text-center text-xl text-neutral-800 font-semibold md:text-left md:text-2xl dark:text-neutral-100">
{{ t('settings.dialogs.onboarding.permissions.title') }}
</h2>
<div h-5 w-5 />
</div>
<div flex-1 overflow-y-auto space-y-4>
<p class="text-sm text-neutral-600 md:text-base dark:text-neutral-300">
{{ t('settings.dialogs.onboarding.permissions.description') }}
</p>
<section class="border border-neutral-200 rounded-xl bg-neutral-50 p-4 dark:border-neutral-700 dark:bg-neutral-800/50">
<div class="mb-3 flex items-start justify-between gap-3">
<div>
<h3 class="text-sm text-neutral-800 font-semibold dark:text-neutral-100">
{{ t('settings.dialogs.onboarding.permissions.notificationsTitle') }}
</h3>
<p class="mt-1 text-xs text-neutral-600 dark:text-neutral-300">
{{ t('settings.dialogs.onboarding.permissions.notificationsDescription') }}
</p>
</div>
<span v-if="notificationPermissionGranted" class="i-solar:check-circle-linear h-5 w-5 text-green-700 dark:text-green-400" />
</div>
<Button
:label="t('settings.dialogs.onboarding.permissions.notificationsAction')"
@click="requestNotificationPermission"
/>
</section>
<p class="text-xs text-neutral-500 dark:text-neutral-400">
{{ t('settings.dialogs.onboarding.permissions.optionalHint') }}
</p>
</div>
<Button
:label="t('settings.dialogs.onboarding.next')"
@click="props.onNext"
/>
</div>
</template>
@@ -322,14 +322,10 @@ watch(enabled, async (val) => {
}
}, { immediate: true })
watch([() => onboardingStore.shouldShowSetup], ([shouldShowSetup]) => {
if (shouldShowSetup) {
onMounted(() => {
if (onboardingStore.needsOnboarding) {
openOnboarding()
}
}, { immediate: true })
onMounted(() => {
onboardingStore.initializeSetupCheck()
})
onUnmounted(() => {
+5 -3
View File
@@ -32,7 +32,7 @@ const onboardingStore = useOnboardingStore()
const chatSessionStore = useChatSessionStore()
const serverChannelStore = useModsServerChannelStore()
const characterOrchestratorStore = useCharacterOrchestratorStore()
const { shouldShowSetup } = storeToRefs(onboardingStore)
const { showingSetup } = storeToRefs(onboardingStore)
const { isDark } = useTheme()
const cardStore = useAiriCardStore()
const analyticsStore = useSharedAnalyticsStore()
@@ -76,7 +76,9 @@ onMounted(async () => {
analyticsStore.initialize()
cardStore.initialize()
onboardingStore.initializeSetupCheck()
if (onboardingStore.needsOnboarding) {
onboardingStore.showingSetup = true
}
await chatSessionStore.initialize()
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
@@ -122,7 +124,7 @@ function handleSetupSkipped() {
<!-- First Time Setup Dialog -->
<OnboardingDialog
v-model="shouldShowSetup"
v-model="showingSetup"
@configured="handleSetupConfigured"
@skipped="handleSetupSkipped"
/>
@@ -32,6 +32,19 @@ dialogs:
no-models-help: >-
Please return to the previous step and check your API key, or check the
network connection.
permissions:
title: Notifications
description: You can allow notifications now or continue and set it up later.
notificationsTitle: Notifications
notificationsDescription: Allow reminders and important status updates.
notificationsAction: Request notification permission
notificationsNotGrantedHint: If denied, you can enable notifications later from iOS Settings.
openSettings: Open iOS Settings
optionalHint: You can continue even if notification permission is not granted.
stateUnknown: Not requested
stateRequesting: Requesting...
stateGranted: Granted
stateNotGranted: Not granted
language:
title: Language
description: >
+9 -41
View File
@@ -1,6 +1,6 @@
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, nextTick, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useProvidersStore } from './providers'
@@ -19,7 +19,7 @@ export const useOnboardingStore = defineStore('onboarding', () => {
const hasSkippedSetup = useLocalStorage('onboarding/skipped', false)
// Track if we should show the setup dialog
const shouldShowSetup = ref(false)
const showingSetup = ref(false)
// Check if any essential provider is configured
const hasEssentialProviderConfigured = computed(() => {
@@ -41,81 +41,49 @@ export const useOnboardingStore = defineStore('onboarding', () => {
})
// Check if first-time setup should be shown
const needsOnboarding = computed(() => {
if (hasSkippedSetup.value) {
console.warn('Onboarding already skipped')
return false
}
const hasConfiguredEssentialProvider = hasEssentialProviderCredentialConfigured.value || hasEssentialProviderConfigured.value
// Recover from stale completed flag (e.g. setup window closed unexpectedly):
// only treat completion as final when an essential provider is actually configured.
if (hasCompletedSetup.value && hasConfiguredEssentialProvider) {
console.warn('Onboarding already completed with configured provider')
return false
}
// Don't show if user already has persisted essential credentials/runtime config.
if (hasConfiguredEssentialProvider) {
console.warn('Essential provider credentials already configured, no onboarding needed')
return false
}
return true
})
const needsOnboarding = computed(() => !hasSkippedSetup.value && !hasCompletedSetup.value)
// Keep in-memory display flag aligned with persisted onboarding status
// when setup is completed/skipped from another window (desktop multi-window case).
watch(needsOnboarding, (needSetup) => {
if (!needSetup) {
shouldShowSetup.value = false
showingSetup.value = false
}
})
// Initialize setup check
async function initializeSetupCheck() {
if (needsOnboarding.value) {
// Use nextTick to ensure the app is fully rendered before showing dialog
await nextTick()
shouldShowSetup.value = true
}
}
// Mark setup as completed
function markSetupCompleted() {
hasCompletedSetup.value = true
hasSkippedSetup.value = false
shouldShowSetup.value = false
showingSetup.value = false
}
// Mark setup as skipped
function markSetupSkipped() {
hasSkippedSetup.value = true
shouldShowSetup.value = false
showingSetup.value = false
}
// Reset setup state (for testing or re-showing setup)
function resetSetupState() {
hasCompletedSetup.value = false
hasSkippedSetup.value = false
shouldShowSetup.value = false
showingSetup.value = false
}
// Force show setup dialog
function forceShowSetup() {
shouldShowSetup.value = true
showingSetup.value = true
}
return {
hasCompletedSetup,
hasSkippedSetup,
shouldShowSetup,
showingSetup,
hasEssentialProviderConfigured,
hasEssentialProviderCredentialConfigured,
needsOnboarding,
initializeSetupCheck,
markSetupCompleted,
markSetupSkipped,
resetSetupState,
+15
View File
@@ -135,6 +135,9 @@ catalogs:
builder-util-runtime:
specifier: ^9.5.1
version: 9.5.1
capacitor-native-settings:
specifier: ^8.0.0
version: 8.0.0
crossws:
specifier: ^0.4.4
version: 0.4.4
@@ -685,6 +688,9 @@ importers:
animejs:
specifier: ^4.3.6
version: 4.3.6
capacitor-native-settings:
specifier: 'catalog:'
version: 8.0.0(@capacitor/core@8.1.0)
colorjs.io:
specifier: ^0.6.1
version: 0.6.1
@@ -10365,6 +10371,11 @@ packages:
resolution: {integrity: sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==}
engines: {node: ^18.12.0 || >= 20.9.0}
capacitor-native-settings@8.0.0:
resolution: {integrity: sha512-a88bZ63dirKsTbylJIo0PBwlOEMOOQmBJyorAr14URo4TA3WqNdQaFfKMF/p1QQ+A0VbtBOi3snLtm/jEK1qzg==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -24700,6 +24711,10 @@ snapshots:
node-addon-api: 7.1.1
prebuild-install: 7.1.3
capacitor-native-settings@8.0.0(@capacitor/core@8.1.0):
dependencies:
'@capacitor/core': 8.1.0
ccount@2.0.1: {}
chai@6.2.2: {}
+1 -2
View File
@@ -10,7 +10,6 @@ packages:
- docs/**
- apps/**
- '!**/dist/**'
overrides:
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
axios: npm:feaxios@^0.0.23
@@ -71,6 +70,7 @@ catalog:
async-mutex: 0.5.0
better-auth: ^1.4.19
builder-util-runtime: ^9.5.1
capacitor-native-settings: ^8.0.0
crossws: ^0.4.4
drizzle-valibot: ^0.4.2
electron: ^40.8.0
@@ -105,7 +105,6 @@ catalog:
xsschema: 0.4.0-beta.13
yaml: ^2.8.2
zod: ^4.3.6
catalogs:
vitest:
'@vitest/browser-playwright': ^4.0.18