Skip to content

Commit

Permalink
fix(native-app): offline mode more fixes (#15246)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
thoreyjona and kodiakhq[bot] authored Jun 17, 2024
1 parent 5bbf5d8 commit f95ab14
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 131 deletions.
263 changes: 137 additions & 126 deletions apps/native/app/src/stores/auth-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AsyncStorage from '@react-native-community/async-storage'
import { Alert } from 'react-native'
import {
authorize,
Expand All @@ -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'
Expand Down Expand Up @@ -59,134 +61,143 @@ const getAppAuthConfig = () => {
}
}

export const authStore = create<AuthStore>((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<AuthStore>(
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)

Expand Down
8 changes: 4 additions & 4 deletions apps/native/app/src/utils/app-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
3 changes: 2 additions & 1 deletion apps/native/app/src/utils/lifecycle/setup-event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down

0 comments on commit f95ab14

Please sign in to comment.