diff --git a/expo/features/app/settings/index.tsx b/expo/features/app/settings/index.tsx index cfe9a541..1fcd0570 100644 --- a/expo/features/app/settings/index.tsx +++ b/expo/features/app/settings/index.tsx @@ -166,4 +166,4 @@ export function useCurrentUserUpdator() { ); } -export { SettingsUserConfigDefault }; +export { SettingsUserConfigDefault, SettingsAppConfig, SettingsUPFCConfig, SettingsUserConfig }; diff --git a/expo/features/app/settings/internals/SettingsProvider.tsx b/expo/features/app/settings/internals/SettingsProvider.tsx index 0f30e1ba..0e71643b 100644 --- a/expo/features/app/settings/internals/SettingsProvider.tsx +++ b/expo/features/app/settings/internals/SettingsProvider.tsx @@ -2,16 +2,16 @@ import SettingsStore from '@hpapp/system/kvs/SettingsStore'; import { createContext, useContext, useMemo, useState, useEffect, useCallback } from 'react'; export interface SettingsContext { - store: Map; + timestamp: number; settingsList: SettingsStore[]; - setValue: (key: string, value: unknown) => void; + refresh: () => void; } const contextObj = createContext({ - store: new Map(), + timestamp: new Date().getTime(), settingsList: [], // eslint-disable-next-line @typescript-eslint/no-unused-vars - setValue: (_key: string, _value: unknown) => {} + refresh: () => {} }); /** @@ -29,31 +29,23 @@ export default function SettingsProvider({ const Provider = contextObj.Provider; const [isLoading, setIsLoading] = useState(true); // hold settings in memory rather than accessing actual storage behind settings list. - const [store, setStore] = useState>(new Map()); + const [timestamp, setTimestamp] = useState(new Date().getTime()); useEffect(() => { // load all settings registered in Settings Store (async () => { - const values = await Promise.all(settings.map(async (s) => await s.load())); - const nextStore = new Map(store); - values.forEach((value, i) => { - const key = settings[i].storageKey; - nextStore.set(key, value); - }); - setStore(nextStore); + await Promise.all(settings.map(async (s) => await s.load())); setIsLoading(false); })(); }, [settings]); const value = useMemo(() => { return { - store, + timestamp, settingsList: settings, - setValue: async (key: string, value: unknown) => { - const nextStore = new Map(store); - nextStore.set(key, value); - setStore(nextStore); + refresh: () => { + setTimestamp(new Date().getTime()); } }; - }, [store, settings]); + }, [timestamp, settings]); if (isLoading) { return null; } @@ -62,23 +54,19 @@ export default function SettingsProvider({ export function useSettings(settings: SettingsStore): [T | undefined, (value: T | null) => Promise] { const ctx = useContext(contextObj); - const value = ctx.store.get(settings.storageKey); const setValue = useCallback( async (value: T | null) => { if (value === null) { await settings.clear(); - ctx.setValue(settings.storageKey, settings.options?.defaultValue); + ctx.refresh(); } else { await settings.save(value); - ctx.setValue(settings.storageKey, value); + ctx.refresh(); } }, - [ctx.setValue, settings] + [ctx, ctx.refresh] ); - if (!ctx.store.has(settings.storageKey)) { - throw new Error(`SettingsStore('${settings.storageKey}') is not initialized with context`); - } - return [value as T, setValue]; + return [settings.current as T, setValue]; } export function useAllSettings(): SettingsStore[] { diff --git a/expo/features/common/form/internals/CalendarDropdown.tsx b/expo/features/common/form/internals/CalendarDropdown.tsx index 02b94ff3..c0c477e5 100644 --- a/expo/features/common/form/internals/CalendarDropdown.tsx +++ b/expo/features/common/form/internals/CalendarDropdown.tsx @@ -25,22 +25,22 @@ export default function CalendarDropdown({ const [calendars, setCalendars] = useState([]); useEffect(() => { (async () => { - let reminderGranted = true; const { granted } = await Calendar.requestCalendarPermissionsAsync(); - if (Platform.OS === 'ios') { - const { granted } = await Calendar.requestRemindersPermissionsAsync(); - reminderGranted = granted; - } - if (granted && reminderGranted) { - const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); - // for Android, getEventsAsync return nothing if the calender is not visible so - // users have to select from only visible calendars. - if (Platform.OS === 'android') { + if (granted) { + if (Platform.OS === 'ios') { + const reminderPermission = await Calendar.requestRemindersPermissionsAsync(); + if (reminderPermission.granted) { + const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); + setCalendars(calendars.filter((c) => c.allowsModifications)); + setIsRequsetingPermission(false); + } + } else if (Platform.OS === 'android') { + const calendars = await Calendar.getCalendarsAsync(); setCalendars(calendars.filter((c) => c.isVisible === true && c.allowsModifications)); + setIsRequsetingPermission(false); } else { - setCalendars(calendars.filter((c) => c.allowsModifications)); + // no calendar support } - setIsRequsetingPermission(false); } })(); }, []); diff --git a/expo/features/devtool/DevtoolScreen.tsx b/expo/features/devtool/DevtoolScreen.tsx index 68facf0e..2802a8c4 100644 --- a/expo/features/devtool/DevtoolScreen.tsx +++ b/expo/features/devtool/DevtoolScreen.tsx @@ -12,6 +12,7 @@ import { useEffect, useState } from 'react'; import { ScrollView } from 'react-native'; import DevtoolListItem from './internals/DevtoolListItem'; +import DevtoolListItemResetOnboardingFlow from './internals/DevtoolListItemResetOnboardingFlow'; import DevtoolUserConfigForm from './internals/DevtoolUserConfigForm'; export default defineScreen('/devtool/', function DevtoolScreen() { @@ -60,7 +61,8 @@ export default defineScreen('/devtool/', function DevtoolScreen() { } /> - + + diff --git a/expo/features/devtool/internals/DevtoolListItemResetOnboardingFlow.tsx b/expo/features/devtool/internals/DevtoolListItemResetOnboardingFlow.tsx new file mode 100644 index 00000000..705cd3b5 --- /dev/null +++ b/expo/features/devtool/internals/DevtoolListItemResetOnboardingFlow.tsx @@ -0,0 +1,33 @@ +import { SettingsAppConfig, SettingsUserConfig, SettingsUPFCConfig } from '@hpapp/features/app/settings'; +import { Text } from '@hpapp/features/common'; +import { clearCacheDir } from '@hpapp/system/uricache'; +import { ListItem } from '@rneui/themed'; +import { Image } from 'expo-image'; +import * as Updates from 'expo-updates'; +import Toast from 'react-native-root-toast'; + +export default function DevtoolListItemResetOnboardingFlow() { + return ( + { + await Promise.all([ + Image.clearDiskCache(), + Image.clearMemoryCache(), + SettingsAppConfig.clear(), + SettingsUserConfig.clear(), + SettingsUPFCConfig.clear(), + clearCacheDir() + ]); + if (__DEV__) { + Toast.show('All data cleared, please reload the app manually', { duration: Toast.durations.SHORT }); + } else { + Updates.reloadAsync(); + } + }} + > + + Reset Onboarding Flow + + + ); +} diff --git a/expo/features/onboarding/OnboardingScreen.tsx b/expo/features/onboarding/OnboardingScreen.tsx index 0471fbe9..ac5d23b4 100644 --- a/expo/features/onboarding/OnboardingScreen.tsx +++ b/expo/features/onboarding/OnboardingScreen.tsx @@ -1,16 +1,13 @@ -import { useUserConfig, useUserConfigUpdator } from '@hpapp/features/app/settings'; -import { defineScreen, useNavigation } from '@hpapp/features/common/stack'; -import HomeScreen from '@hpapp/features/home/HomeScreen'; +import { defineScreen } from '@hpapp/features/common/stack'; import { t } from '@hpapp/system/i18n'; import OnboardingStepFollowMembers from './internals/OnboardingStepFollowMembers'; import OnboardingStepProvider from './internals/OnboardingStepProvider'; -import OnboardinStepUPCSettings from './internals/OnboardingStepUPFCSettings'; +import OnboardingStepUPCSettings from './internals/OnboardingStepUPFCSettings'; +import useCompleteOnboarding from './internals/useCompleteOnboarding'; export default defineScreen('/onboarding/', function OnboardingScreen() { - const navigation = useNavigation(); - const userConfig = useUserConfig(); - const userConfigUpdate = useUserConfigUpdator(); + const completeOnboarding = useCompleteOnboarding(); return ( { - userConfigUpdate({ ...userConfig!, completeOnboarding: true }); - navigation.replace(HomeScreen); + completeOnboarding({ + set_upfc: true + }); }} /> ) diff --git a/expo/features/onboarding/internals/OnboardingStepProvider.tsx b/expo/features/onboarding/internals/OnboardingStepProvider.tsx index e43361d8..160127eb 100644 --- a/expo/features/onboarding/internals/OnboardingStepProvider.tsx +++ b/expo/features/onboarding/internals/OnboardingStepProvider.tsx @@ -1,10 +1,9 @@ -import { useUserConfig, useUserConfigUpdator } from '@hpapp/features/app/settings'; -import { useNavigation, useScreenTitle } from '@hpapp/features/common/stack'; -import HomeScreen from '@hpapp/features/home/HomeScreen'; +import { useScreenTitle } from '@hpapp/features/common/stack'; import { t } from '@hpapp/system/i18n'; import { useState } from 'react'; import OnboardingStep, { OnboardingStepProps } from './OnboardingStep'; +import useCompleteOnboarding from './useCompleteOnboarding'; export type OnboardingStepContainerProps = { firstStep: string; @@ -18,9 +17,7 @@ export type OnboardingStepContainerProps = { export default function OnboardingStepContainer({ firstStep, steps }: OnboardingStepContainerProps) { useScreenTitle(t('Welcome to Hello!Fan App!')); - const navigation = useNavigation(); - const userConfig = useUserConfig(); - const userConfigUpdate = useUserConfigUpdator(); + const completeOnboarding = useCompleteOnboarding(); const [stack, setStack] = useState([firstStep]); return ( <> @@ -35,8 +32,7 @@ export default function OnboardingStepContainer({ firstStep, steps }: Onboarding setStack((prev) => [next, ...prev]); } : () => { - userConfigUpdate({ ...userConfig!, completeOnboarding: true }); - navigation.replace(HomeScreen); + completeOnboarding(); }; const onBackPress = stack.length > 1 ? () => setStack((prev) => prev.slice(1)) : undefined; const nextText = next ? t('Next') : t('Complete'); diff --git a/expo/features/onboarding/internals/useCompleteOnboarding.ts b/expo/features/onboarding/internals/useCompleteOnboarding.ts new file mode 100644 index 00000000..5abe3fbd --- /dev/null +++ b/expo/features/onboarding/internals/useCompleteOnboarding.ts @@ -0,0 +1,17 @@ +import { useUserConfig, useUserConfigUpdator } from '@hpapp/features/app/settings'; +import { useNavigation } from '@hpapp/features/common/stack'; +import HomeScreen from '@hpapp/features/home/HomeScreen'; +import { logEvent } from '@hpapp/system/firebase'; +import * as logging from 'system/logging'; + +export default function useCompleteOnboarding() { + const navigation = useNavigation(); + const userConfig = useUserConfig(); + const userConfigUpdate = useUserConfigUpdator(); + return function completeOnboarding(params?: Record) { + userConfigUpdate({ ...userConfig!, completeOnboarding: true }); + logging.Info('features.onboarding.useCompleteOnboarding', 'onboarding completed', params); + logEvent('onboarding_complete', params); + navigation.replace(HomeScreen); + }; +} diff --git a/expo/features/upfc/internals/settings/UPFCSettingsForm.tsx b/expo/features/upfc/internals/settings/UPFCSettingsForm.tsx index 27c7b4a4..8d779e4e 100644 --- a/expo/features/upfc/internals/settings/UPFCSettingsForm.tsx +++ b/expo/features/upfc/internals/settings/UPFCSettingsForm.tsx @@ -72,13 +72,7 @@ export default function UPFCSettingsForm({ onSave }: UPFCSettingsFormProps) { } lastAuthenticatedAt = new Date().getTime() / 1000; } - Toast.show(t('Saved Successfully!'), { - position: Toast.positions.BOTTOM, - duration: Toast.durations.SHORT, - textColor: succsesContrast, - backgroundColor: successColor - }); - updateConfig({ + await updateConfig({ hpUsername, hpPassword, mlUsername, @@ -87,13 +81,19 @@ export default function UPFCSettingsForm({ onSave }: UPFCSettingsFormProps) { eventPrefix, lastAuthenticatedAt }); + Toast.show(t('Saved Successfully!'), { + position: Toast.positions.BOTTOM, + duration: Toast.durations.SHORT, + textColor: succsesContrast, + backgroundColor: successColor + }); + onSave && onSave(); } catch (e) { setLastError(e as Error); return; } finally { setIsSaving(false); } - onSave && onSave(); })(); }, [updateConfig, hpUsername, hpPassword, mlUsername, mlPassword, calendarId, eventPrefix]); return ( diff --git a/expo/system/kvs/SettingsStore.ts b/expo/system/kvs/SettingsStore.ts index 4c4371f5..6c3e4514 100644 --- a/expo/system/kvs/SettingsStore.ts +++ b/expo/system/kvs/SettingsStore.ts @@ -1,4 +1,5 @@ import { Platform } from 'react-native'; +import * as logging from 'system/logging'; import JSONStore from './JSONStore'; import LocalStorage from './LocalStorage'; @@ -93,6 +94,7 @@ export default class SettingsStore { }, value: data }); + this.data = data; this.loaded = true; } @@ -101,4 +103,17 @@ export default class SettingsStore { this.data = undefined; this.loaded = true; } + + get current(): T | undefined { + if (!this.loaded) { + logging.Error( + 'system.kvs.SettingsStore.current', + 'SettingsStore is not loaded yet, fallback to undefined or defaultValue', + { + key: this.storageKey + } + ); + } + return this.data ?? this.options?.defaultValue; + } }