Skip to content

Commit

Permalink
[expo] refactoring and bug fixes
Browse files Browse the repository at this point in the history
**Summary**

- refactor calendar dropdown to clarify Android/iOS behavior.

- fix the SettingsProvider context bug when multiple instances call `setValue` at the same time.

We now removed memory map from the context value in `SettingsProvider`, but use `current` value from `SettingsStore` directly.

- add complete onboarding logging.

**Test**

- expo

**Issue**

- N/A
  • Loading branch information
yssk22 committed Jan 6, 2025
1 parent 0a90094 commit 51b2815
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 66 deletions.
2 changes: 1 addition & 1 deletion expo/features/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,4 @@ export function useCurrentUserUpdator() {
);
}

export { SettingsUserConfigDefault };
export { SettingsUserConfigDefault, SettingsAppConfig, SettingsUPFCConfig, SettingsUserConfig };
40 changes: 14 additions & 26 deletions expo/features/app/settings/internals/SettingsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
timestamp: number;
settingsList: SettingsStore<unknown>[];
setValue: (key: string, value: unknown) => void;
refresh: () => void;
}

const contextObj = createContext<SettingsContext>({
store: new Map(),
timestamp: new Date().getTime(),
settingsList: [],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setValue: (_key: string, _value: unknown) => {}
refresh: () => {}
});

/**
Expand All @@ -29,31 +29,23 @@ export default function SettingsProvider({
const Provider = contextObj.Provider;
const [isLoading, setIsLoading] = useState<boolean>(true);
// hold settings in memory rather than accessing actual storage behind settings list.
const [store, setStore] = useState<Map<string, unknown>>(new Map());
const [timestamp, setTimestamp] = useState<number>(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;
}
Expand All @@ -62,23 +54,19 @@ export default function SettingsProvider({

export function useSettings<T>(settings: SettingsStore<T>): [T | undefined, (value: T | null) => Promise<void>] {
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<unknown>[] {
Expand Down
24 changes: 12 additions & 12 deletions expo/features/common/form/internals/CalendarDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ export default function CalendarDropdown({
const [calendars, setCalendars] = useState<Calendar.Calendar[]>([]);
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);
}
})();
}, []);
Expand Down
4 changes: 3 additions & 1 deletion expo/features/devtool/DevtoolScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -60,7 +61,8 @@ export default defineScreen('/devtool/', function DevtoolScreen() {
}
/>
<Divider />

<DevtoolListItemResetOnboardingFlow />
<Divider />
<ListItemClearCache />
<Divider />
<DevtoolUserConfigForm />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ListItem
onPress={async () => {
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();
}
}}
>
<ListItem.Content>
<Text>Reset Onboarding Flow</Text>
</ListItem.Content>
</ListItem>
);
}
18 changes: 8 additions & 10 deletions expo/features/onboarding/OnboardingScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<OnboardingStepProvider
firstStep="followMembers"
Expand All @@ -35,10 +32,11 @@ export default defineScreen('/onboarding/', function OnboardingScreen() {
t('You can skip this step to complete the initial settings.')
].join(' '),
element: (
<OnboardinStepUPCSettings
<OnboardingStepUPCSettings
onSave={() => {
userConfigUpdate({ ...userConfig!, completeOnboarding: true });
navigation.replace(HomeScreen);
completeOnboarding({
set_upfc: true
});
}}
/>
)
Expand Down
12 changes: 4 additions & 8 deletions expo/features/onboarding/internals/OnboardingStepProvider.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<>
Expand All @@ -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');
Expand Down
17 changes: 17 additions & 0 deletions expo/features/onboarding/internals/useCompleteOnboarding.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>) {
userConfigUpdate({ ...userConfig!, completeOnboarding: true });
logging.Info('features.onboarding.useCompleteOnboarding', 'onboarding completed', params);
logEvent('onboarding_complete', params);
navigation.replace(HomeScreen);
};
}
16 changes: 8 additions & 8 deletions expo/features/upfc/internals/settings/UPFCSettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down
15 changes: 15 additions & 0 deletions expo/system/kvs/SettingsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Platform } from 'react-native';
import * as logging from 'system/logging';

import JSONStore from './JSONStore';
import LocalStorage from './LocalStorage';
Expand Down Expand Up @@ -93,6 +94,7 @@ export default class SettingsStore<T> {
},
value: data
});
this.data = data;
this.loaded = true;
}

Expand All @@ -101,4 +103,17 @@ export default class SettingsStore<T> {
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;
}
}

0 comments on commit 51b2815

Please sign in to comment.