Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(native-app): offline mode more fixes #15246

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
thoreyjona marked this conversation as resolved.
Show resolved Hide resolved
},
)

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'],
},
thoreyjona marked this conversation as resolved.
Show resolved Hide resolved
),
)

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)
thoreyjona marked this conversation as resolved.
Show resolved Hide resolved

// reset lockscreen parameters
authStore.setState({
lockScreenActivatedAt: undefined,
lockScreenActivatedAt: null,
lockScreenComponentId: undefined,
})
}
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 &&
thoreyjona marked this conversation as resolved.
Show resolved Hide resolved
lockScreenActivatedAt + appLockTimeout > Date.now()
) {
hideAppLockOverlay()
hideAppLockOverlay(lockScreenComponentId)
} else {
Navigation.updateProps(lockScreenComponentId, { status })
}
Expand Down
Loading