From d1d34a30684de344c039076a8549bc4d5eaf2c2e Mon Sep 17 00:00:00 2001 From: Amaury <1293565+amaurym@users.noreply.github.com> Date: Sat, 6 Mar 2021 22:08:58 +0100 Subject: [PATCH] fix: Delete user when no more notifications (#914) * update common * Fix optimistic UI --- .../SelectNotifications.tsx | 90 ++++++++++--------- App/util/axios.ts | 16 ++++ App/util/sentry.ts | 4 +- package.json | 2 +- yarn.lock | 18 ++-- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx index ddc6ca0e..0b5482de 100644 --- a/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx +++ b/App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx @@ -33,16 +33,22 @@ import { t } from '../../../../localization'; import { ApiContext } from '../../../../stores'; import { AmplitudeEvent, track } from '../../../../util/amplitude'; import { promiseToTE } from '../../../../util/fp'; -import { createUser, getUser, updateUser } from '../../../../util/axios'; +import { + createUser, + deleteUser, + getUser, + updateUser, +} from '../../../../util/axios'; import { sentryError } from '../../../../util/sentry'; import * as theme from '../../../../util/theme'; const notificationsValues = ['never', 'daily', 'weekly', 'monthly'] as const; /** - * Key in the AsyncStorage for the mongo userId. + * Key in the AsyncStorage for the last chosen frequency. It's used for + * optimistic UI in the frequency selector. */ -const KEY_MONGO_USER = 'mongoUser'; +const KEY_LAST_CHOSEN_FREQUENCY = 'lastChosenFrequency'; /** * Capitalize a string. @@ -111,55 +117,53 @@ export function SelectNotifications( // Fetch data from backend. useEffect(() => { - AsyncStorage.getItem(KEY_MONGO_USER) - .then((serializedUser) => { - if (serializedUser) { - const user = JSON.parse(serializedUser) as MongoUser; - console.log( - ` - Got user ${JSON.stringify( - user - )} from AsyncStorage.` - ); - - setCurrentUser(user); - - if (!user.expoReport?.expoPushToken) { + AsyncStorage.getItem(KEY_LAST_CHOSEN_FREQUENCY) + .then((lastChosenFrequency) => { + if (lastChosenFrequency) { + if ( + !(notificationsValues as readonly string[]).includes( + lastChosenFrequency + ) + ) { throw new Error( - ' - User in AsyncStorage does not have expoPushToken.' + ` - Got unknown frequency "${lastChosenFrequency}" from AsyncStorage.` ); } - // Retrieve from backend the latest version of current user. - return getUser(user.expoReport.expoPushToken); - } else { + const f = lastChosenFrequency as Frequency; console.log( - ' - No user found in AsyncStorage.' + ` - Got frequency "${f}" from AsyncStorage.` ); - // If the user have gave the "Notifications" permission, - // then we fetch the user from the backend. - return Permissions.getAsync( - Permissions.NOTIFICATIONS - ).then(({ status }) => - status === 'granted' - ? Notifications.getExpoPushTokenAsync().then( - ({ data }) => getUser(data) - ) - : undefined - ); + // We're optimistic that the backend frequency is the same + // as the one saved in AsyncStorage, so we set it. + setOptimisticNotif(f); } }) + .then(() => { + return Permissions.getAsync( + Permissions.NOTIFICATIONS + ).then(({ status }) => + status === 'granted' + ? Notifications.getExpoPushTokenAsync().then( + ({ data }) => getUser(data) + ) + : undefined + ); + }) .then((user) => user && setCurrentUser(user)) .catch(sentryError('SelectNotifications')); }, []); - // Update currentUser in AsyncStorage each time it changes. + // Update lastChosenFrequency in AsyncStorage each time it changes. useEffect(() => { - currentUser && - AsyncStorage.setItem( - KEY_MONGO_USER, - JSON.stringify(currentUser) - ).catch(sentryError('SelectNotifications')); + (currentUser?.expoReport?.frequency + ? AsyncStorage.setItem( + KEY_LAST_CHOSEN_FREQUENCY, + currentUser.expoReport.frequency + ) + : AsyncStorage.removeItem(KEY_LAST_CHOSEN_FREQUENCY) + ).catch(sentryError('SelectNotifications')); }, [currentUser]); // This state is used for optimistic UI: right after the user clicks, we set @@ -254,9 +258,9 @@ export function SelectNotifications( } } else { if (notifications.frequency === 'never') { - return updateUser(currentUser._id, { - expoReport: (null as unknown) as undefined, - }); + return deleteUser(currentUser._id).then( + () => undefined // Set currentUser to undefined after deletion. + ); } else { return updateUser(currentUser._id, { expoReport: { @@ -270,7 +274,7 @@ export function SelectNotifications( ), TE.chain( sideEffect((user) => { - user && setCurrentUser(user); + setCurrentUser(user); return TE.right(undefined); }) @@ -278,7 +282,7 @@ export function SelectNotifications( TE.fold( (error) => { sentryError('SelectNotifications')(error); - setOptimisticNotif('never'); + setOptimisticNotif(undefined); track('HOME_SCREEN_NOTIFICATIONS_ERROR'); diff --git a/App/util/axios.ts b/App/util/axios.ts index 10ce0eb5..5fe7be1f 100644 --- a/App/util/axios.ts +++ b/App/util/axios.ts @@ -83,6 +83,22 @@ export function updateUser( }); } +export function deleteUser(userId: string): Promise { + console.log(` - DELETE /api/users/${userId}`); + + return axios + .delete( + `${ + Constants.manifest.extra.backendUrl as string + }/api/users/${userId}`, + axiosConfig + ) + .then(({ data }) => data) + .catch((err) => { + throw axiosErrorToError(err); + }); +} + function axiosErrorToError(err: AxiosError) { return new Error(`${err.message}: ${JSON.stringify(err.response?.data)}`); } diff --git a/App/util/sentry.ts b/App/util/sentry.ts index 60b39fea..865c6289 100644 --- a/App/util/sentry.ts +++ b/App/util/sentry.ts @@ -31,9 +31,9 @@ const UNTRACKED_ERRORS = [ // No results from data providers 'does not have PM2.5 measurings right now', 'Cannot normalize, got 0 result', - 'Request to fetch API data timed out.', + 'Request to fetch API data timed out', // First time fetching a user - 'No user with "ExponentPushToken', + 'No user with', ]; /** diff --git a/package.json b/package.json index 49f92972..96b4fd96 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@react-native-community/masked-view": "0.1.10", "@react-navigation/native": "^5.9.3", "@react-navigation/stack": "^5.14.3", - "@shootismoke/ui": "^0.8.7", + "@shootismoke/ui": "^0.8.8", "@types/i18n-js": "^3.8.0", "@types/react-native": "~0.63.2", "date-fns": "^2.17.0", diff --git a/yarn.lock b/yarn.lock index 0b01821e..2a0064ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2492,10 +2492,10 @@ "@types/debug" "^4.1.5" debug "^4.1.1" -"@shootismoke/dataproviders@^0.8.7": - version "0.8.7" - resolved "https://registry.yarnpkg.com/@shootismoke/dataproviders/-/dataproviders-0.8.7.tgz#f17ee14b640ef9fbb5bd950916a85fe56401fdf7" - integrity sha512-6leoUJNyoB8GCFq7D/3SWH/tKEu5XHct0u/+IlwhMKfI3bVg1ZjnbMJF7WHLd5a7okGP5yiBIUAAbz3I2AaU3Q== +"@shootismoke/dataproviders@^0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@shootismoke/dataproviders/-/dataproviders-0.8.8.tgz#fdfae6d0a74be8778703b8a23dcb6f7d91dbbc98" + integrity sha512-yxVmcWFAvXbCvS2e5ZExv05/1+pjIW/W1zS0eUwks4EdpT1TkOeZcA5TLs3WpB+QYIKCYJV2Ca4j82fQmvDihA== dependencies: "@shootismoke/convert" "^0.8.7" axios "^0.21.0" @@ -2504,12 +2504,12 @@ fp-ts "^2.1.1" io-ts "^2.0.1" -"@shootismoke/ui@^0.8.7": - version "0.8.7" - resolved "https://registry.yarnpkg.com/@shootismoke/ui/-/ui-0.8.7.tgz#0fbf47a132f159b2fa14a7f78abb7891df1069e5" - integrity sha512-cPejm5ZPQC25171bXXftUqEnhkAPmMOAtexXx8cFnM3gVlbckq6TEHsjeQZXDWyko+L1NaemUt5g7SPYQmEugQ== +"@shootismoke/ui@^0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@shootismoke/ui/-/ui-0.8.8.tgz#7a2f0a56a798a128380554b4452de504cade793a" + integrity sha512-Ep/Uh2t2s7dmkpXE2Lf/4GHwP3gGMmLQ2atlR9X/CBfD30m+BTz8vqcvbgUN89nRoMZYHCHHalj28IEjJ17sOQ== dependencies: - "@shootismoke/dataproviders" "^0.8.7" + "@shootismoke/dataproviders" "^0.8.8" "@sindresorhus/slugify" "^1.1.0" "@types/async-retry" "^1.4.2" "@types/haversine" "^1.1.4"