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:
co-authored by
autofix-ci[bot]
parent
41ce282e95
commit
589553c0e3
@@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+15
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user