From f95ab14dbb0647f33acf94642f062ffee8621865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 17 Jun 2024 20:18:31 +0000 Subject: [PATCH] fix(native-app): offline mode more fixes (#15246) * fix: show offline banner when going offline with app active * feat: persist userInfo --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/stores/auth-store.ts | 263 +++++++++--------- apps/native/app/src/utils/app-lock.ts | 8 +- .../utils/lifecycle/setup-event-handlers.ts | 3 +- 3 files changed, 143 insertions(+), 131 deletions(-) diff --git a/apps/native/app/src/stores/auth-store.ts b/apps/native/app/src/stores/auth-store.ts index 239ad7c42b4b..b55f738d1b4e 100644 --- a/apps/native/app/src/stores/auth-store.ts +++ b/apps/native/app/src/stores/auth-store.ts @@ -1,3 +1,4 @@ +import AsyncStorage from '@react-native-community/async-storage' import { Alert } from 'react-native' import { authorize, @@ -9,6 +10,7 @@ import { import Keychain from 'react-native-keychain' import createUse from 'zustand' import create, { State } from 'zustand/vanilla' +import { persist } from 'zustand/middleware' import { bundleId, getConfig } from '../config' import { getIntl } from '../contexts/i18n-provider' import { getApolloClientAsync } from '../graphql/client' @@ -59,134 +61,143 @@ const getAppAuthConfig = () => { } } -export const authStore = create((set, get) => ({ - authorizeResult: undefined, - userInfo: undefined, - lockScreenActivatedAt: undefined, - lockScreenComponentId: undefined, - noLockScreenUntilNextAppStateActive: false, - isCogitoAuth: false, - cognitoDismissCount: 0, - cognitoAuthUrl: undefined, - cookies: '', - async fetchUserInfo(skipRefresh = false) { - const appAuthConfig = getAppAuthConfig() - // Detect expired token - const expiresAt = get().authorizeResult?.accessTokenExpirationDate ?? 0 - - if (!skipRefresh && new Date(expiresAt) < new Date()) { - await get().refresh() - } - - const res = await fetch( - `${appAuthConfig.issuer.replace(/\/$/, '')}/connect/userinfo`, - { - headers: { - Authorization: `Bearer ${get().authorizeResult?.accessToken}`, - }, +export const authStore = create( + persist( + (set, get) => ({ + authorizeResult: undefined, + userInfo: undefined, + lockScreenActivatedAt: undefined, + lockScreenComponentId: undefined, + noLockScreenUntilNextAppStateActive: false, + isCogitoAuth: false, + cognitoDismissCount: 0, + cognitoAuthUrl: undefined, + cookies: '', + async fetchUserInfo(skipRefresh = false) { + const appAuthConfig = getAppAuthConfig() + // Detect expired token + const expiresAt = get().authorizeResult?.accessTokenExpirationDate ?? 0 + + if (!skipRefresh && new Date(expiresAt) < new Date()) { + await get().refresh() + } + + const res = await fetch( + `${appAuthConfig.issuer.replace(/\/$/, '')}/connect/userinfo`, + { + headers: { + Authorization: `Bearer ${get().authorizeResult?.accessToken}`, + }, + }, + ) + + if (res.status === 401) { + // Attempt to refresh the access token + if (!skipRefresh) { + await get().refresh() + // Retry the userInfo call + return get().fetchUserInfo(true) + } + throw new Error(UNAUTHORIZED_USER_INFO) + } else if (res.status === 200) { + const userInfo = await res.json() + set({ userInfo }) + + return userInfo + } }, - ) - - if (res.status === 401) { - // Attempt to refresh the access token - if (!skipRefresh) { - await get().refresh() - // Retry the userInfo call - return get().fetchUserInfo(true) - } - throw new Error(UNAUTHORIZED_USER_INFO) - } else if (res.status === 200) { - const userInfo = await res.json() - set({ userInfo }) - - return userInfo - } - }, - async refresh() { - const appAuthConfig = getAppAuthConfig() - const refreshToken = get().authorizeResult?.refreshToken - - if (!refreshToken) { - return - } - - const newAuthorizeResult = await authRefresh(appAuthConfig, { - refreshToken, - }) - - const authorizeResult = { - ...get().authorizeResult, - ...newAuthorizeResult, - } - - await Keychain.setGenericPassword( - KEYCHAIN_AUTH_KEY, - JSON.stringify(authorizeResult), - { service: KEYCHAIN_AUTH_KEY }, - ) - set({ authorizeResult }) - }, - async login() { - const appAuthConfig = getAppAuthConfig() - const authorizeResult = await authorize({ - ...appAuthConfig, - additionalParameters: { - prompt: 'login', - prompt_delegations: 'true', - ui_locales: preferencesStore.getState().locale, - externalUserAgent: 'yes', + async refresh() { + const appAuthConfig = getAppAuthConfig() + const refreshToken = get().authorizeResult?.refreshToken + + if (!refreshToken) { + return + } + + const newAuthorizeResult = await authRefresh(appAuthConfig, { + refreshToken, + }) + + const authorizeResult = { + ...get().authorizeResult, + ...newAuthorizeResult, + } + + await Keychain.setGenericPassword( + KEYCHAIN_AUTH_KEY, + JSON.stringify(authorizeResult), + { service: KEYCHAIN_AUTH_KEY }, + ) + set({ authorizeResult }) }, - }) - - if (authorizeResult) { - await Keychain.setGenericPassword( - KEYCHAIN_AUTH_KEY, - JSON.stringify(authorizeResult), - { service: KEYCHAIN_AUTH_KEY }, - ) - set({ authorizeResult }) - return true - } - return false - }, - async logout() { - // Clear all MMKV storages - clearAllStorages() - - // Clear push token if exists - const pushToken = notificationsStore.getState().pushToken - if (pushToken) { - notificationsStore.getState().deletePushToken(pushToken) - } - notificationsStore.getState().reset() - - const appAuthConfig = getAppAuthConfig() - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const tokenToRevoke = get().authorizeResult!.accessToken! - try { - await revoke(appAuthConfig, { - tokenToRevoke, - includeBasicAuth: true, - sendClientId: true, - }) - } catch (e) { - // NOOP - } - - const client = await getApolloClientAsync() - await client.cache.reset() - await Keychain.resetGenericPassword({ service: KEYCHAIN_AUTH_KEY }) - set( - (state) => ({ - ...state, - authorizeResult: undefined, - userInfo: undefined, - }), - true, - ) - return true - }, -})) + async login() { + const appAuthConfig = getAppAuthConfig() + const authorizeResult = await authorize({ + ...appAuthConfig, + additionalParameters: { + prompt: 'login', + prompt_delegations: 'true', + ui_locales: preferencesStore.getState().locale, + externalUserAgent: 'yes', + }, + }) + + if (authorizeResult) { + await Keychain.setGenericPassword( + KEYCHAIN_AUTH_KEY, + JSON.stringify(authorizeResult), + { service: KEYCHAIN_AUTH_KEY }, + ) + set({ authorizeResult }) + return true + } + return false + }, + async logout() { + // Clear all MMKV storages + clearAllStorages() + + // Clear push token if exists + const pushToken = notificationsStore.getState().pushToken + if (pushToken) { + notificationsStore.getState().deletePushToken(pushToken) + } + notificationsStore.getState().reset() + + const appAuthConfig = getAppAuthConfig() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tokenToRevoke = get().authorizeResult!.accessToken! + try { + await revoke(appAuthConfig, { + tokenToRevoke, + includeBasicAuth: true, + sendClientId: true, + }) + } catch (e) { + // NOOP + } + + const client = await getApolloClientAsync() + await client.cache.reset() + await Keychain.resetGenericPassword({ service: KEYCHAIN_AUTH_KEY }) + set( + (state) => ({ + ...state, + authorizeResult: undefined, + userInfo: undefined, + }), + true, + ) + return true + }, + }), + { + name: 'auth_01', + getStorage: () => AsyncStorage, + whitelist: ['userInfo'], + }, + ), +) export const useAuthStore = createUse(authStore) diff --git a/apps/native/app/src/utils/app-lock.ts b/apps/native/app/src/utils/app-lock.ts index 782351abe2f6..e0e712f83c29 100644 --- a/apps/native/app/src/utils/app-lock.ts +++ b/apps/native/app/src/utils/app-lock.ts @@ -55,13 +55,13 @@ export function showAppLockOverlay({ }) } -export function hideAppLockOverlay() { - // Dismiss all overlays - Navigation.dismissAllOverlays() +export function hideAppLockOverlay(lockScreenComponentId: string) { + // Dismiss the lock screen + Navigation.dismissOverlay(lockScreenComponentId) // reset lockscreen parameters authStore.setState({ - lockScreenActivatedAt: undefined, + lockScreenActivatedAt: null, lockScreenComponentId: undefined, }) } diff --git a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts index eaf73bdd70d2..c72d7ae829ef 100644 --- a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts +++ b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts @@ -126,9 +126,10 @@ export function setupEventHandlers() { if (lockScreenComponentId) { if ( lockScreenActivatedAt !== undefined && + lockScreenActivatedAt !== null && lockScreenActivatedAt + appLockTimeout > Date.now() ) { - hideAppLockOverlay() + hideAppLockOverlay(lockScreenComponentId) } else { Navigation.updateProps(lockScreenComponentId, { status }) }