diff --git a/packages/legacy/core/App/contexts/auth.tsx b/packages/legacy/core/App/contexts/auth.tsx index 54355e0f78..66802f0790 100644 --- a/packages/legacy/core/App/contexts/auth.tsx +++ b/packages/legacy/core/App/contexts/auth.tsx @@ -24,7 +24,7 @@ import { } from '../services/keychain' import { WalletSecret } from '../types/security' import { hashPIN } from '../utils/crypto' -import { didMigrateToAskar, migrateToAskar } from '../utils/migration' +import { migrateToAskar } from '../utils/migration' export interface AuthContext { checkPIN: (PIN: string) => Promise @@ -101,7 +101,7 @@ export const AuthProvider: React.FC = ({ children }) => const hash = await hashPIN(PIN, secret.salt) - if (!didMigrateToAskar(store.migration)) { + if (!store.migration.didMigrateToAskar) { await migrateToAskar(secret.id, hash) dispatch({ type: DispatchAction.DID_MIGRATE_TO_ASKAR, @@ -128,7 +128,7 @@ export const AuthProvider: React.FC = ({ children }) => } catch (e) { return false } - }, [dispatch, store.migration]) + }, [dispatch, store.migration.didMigrateToAskar]) const removeSavedWalletSecret = useCallback(() => { setWalletSecret(undefined) diff --git a/packages/legacy/core/App/hooks/initialize-agent.ts b/packages/legacy/core/App/hooks/initialize-agent.ts new file mode 100644 index 0000000000..87b8d87697 --- /dev/null +++ b/packages/legacy/core/App/hooks/initialize-agent.ts @@ -0,0 +1,175 @@ +import { Agent, HttpOutboundTransport, WsOutboundTransport, WalletError } from '@credo-ts/core' +import { IndyVdrPoolService } from '@credo-ts/indy-vdr/build/pool' +import { useAgent } from '@credo-ts/react-hooks' +import { agentDependencies } from '@credo-ts/react-native' +import { GetCredentialDefinitionRequest, GetSchemaRequest } from '@hyperledger/indy-vdr-shared' +import { useCallback } from 'react' +import { Config } from 'react-native-config' +import { CachesDirectoryPath } from 'react-native-fs' + +import { TOKENS, useServices } from '../container-api' +import { useAuth } from '../contexts/auth' +import { DispatchAction } from '../contexts/reducers/store' +import { useStore } from '../contexts/store' +import { BifoldError } from '../types/error' +import { getAgentModules, createLinkSecretIfRequired } from '../utils/agent' +import { migrateToAskar } from '../utils/migration' + +const useInitializeAgent = () => { + const { agent, setAgent } = useAgent() + const [store, dispatch] = useStore() + const { walletSecret } = useAuth() + const [cacheSchemas, cacheCredDefs, logger, indyLedgers] = useServices([ + TOKENS.CACHE_SCHEMAS, + TOKENS.CACHE_CRED_DEFS, + TOKENS.UTIL_LOGGER, + TOKENS.UTIL_LEDGERS, + ]) + + const restartExistingAgent = useCallback(async () => { + if (!walletSecret?.id || !walletSecret.key || !agent) { + return + } + + logger.info('Agent already initialized, restarting...') + + try { + await agent.wallet.open({ + id: walletSecret.id, + key: walletSecret.key, + }) + + logger.info('Opened agent wallet') + } catch (error: unknown) { + // Credo does not use error codes but this will be in the + // the error message if the wallet is already open. + const catchPhrase = 'instance already opened' + + if (error instanceof WalletError && error.message.includes(catchPhrase)) { + logger.warn('Wallet already open, nothing to do') + } else { + logger.error('Error opening existing wallet:', error as Error) + + throw new BifoldError( + 'Wallet Service', + 'There was a problem unlocking the wallet.', + (error as Error).message, + 1047 + ) + } + } + + await agent.mediationRecipient.initiateMessagePickup() + }, [agent, walletSecret, logger]) + + const createNewAgent = useCallback(async (): Promise => { + if (!walletSecret?.id || !walletSecret.key) { + return + } + + logger.info('No agent initialized, creating a new one') + + const newAgent = new Agent({ + config: { + label: store.preferences.walletName || 'Aries Bifold', + walletConfig: { + id: walletSecret.id, + key: walletSecret.key, + }, + logger, + autoUpdateStorageOnStartup: true, + }, + dependencies: agentDependencies, + modules: getAgentModules({ + indyNetworks: indyLedgers, + mediatorInvitationUrl: Config.MEDIATOR_URL, + txnCache: { + capacity: 1000, + expiryOffsetMs: 1000 * 60 * 60 * 24 * 7, + path: CachesDirectoryPath + '/txn-cache', + }, + }), + }) + const wsTransport = new WsOutboundTransport() + const httpTransport = new HttpOutboundTransport() + + newAgent.registerOutboundTransport(wsTransport) + newAgent.registerOutboundTransport(httpTransport) + + return newAgent + }, [walletSecret, store.preferences.walletName, logger, indyLedgers]) + + const migrateIfRequired = useCallback(async (newAgent: Agent) => { + if (!walletSecret?.id || !walletSecret.key) { + return + } + + // If we haven't migrated to Aries Askar yet, we need to do this before we initialize the agent. + if (!store.migration.didMigrateToAskar) { + newAgent.config.logger.debug('Agent not updated to Aries Askar, updating...') + + await migrateToAskar(walletSecret.id, walletSecret.key, newAgent) + + newAgent.config.logger.debug('Successfully finished updating agent to Aries Askar') + // Store that we migrated to askar. + dispatch({ + type: DispatchAction.DID_MIGRATE_TO_ASKAR, + }) + } + }, [walletSecret, store.migration.didMigrateToAskar, dispatch]) + + const warmUpCache = useCallback(async (newAgent: Agent) => { + const poolService = newAgent.dependencyManager.resolve(IndyVdrPoolService) + cacheCredDefs.forEach(async ({ did, id }) => { + const pool = await poolService.getPoolForDid(newAgent.context, did) + const credDefRequest = new GetCredentialDefinitionRequest({ credentialDefinitionId: id }) + await pool.pool.submitRequest(credDefRequest) + }) + + cacheSchemas.forEach(async ({ did, id }) => { + const pool = await poolService.getPoolForDid(newAgent.context, did) + const schemaRequest = new GetSchemaRequest({ schemaId: id }) + await pool.pool.submitRequest(schemaRequest) + }) + }, [cacheCredDefs, cacheSchemas]) + + const initializeAgent = useCallback(async (): Promise => { + if (!walletSecret?.id || !walletSecret.key) { + return + } + + if (agent) { + await restartExistingAgent() + return agent + } + + const newAgent = await createNewAgent() + if (!newAgent) { + return + } + + await migrateIfRequired(newAgent) + + await newAgent.initialize() + + await createLinkSecretIfRequired(newAgent) + + await warmUpCache(newAgent) + + setAgent(newAgent) + + return newAgent + }, [ + agent, + setAgent, + walletSecret, + restartExistingAgent, + createNewAgent, + migrateIfRequired, + warmUpCache, + ]) + + return { initializeAgent } +} + +export default useInitializeAgent diff --git a/packages/legacy/core/App/index.ts b/packages/legacy/core/App/index.ts index 42fe4869ad..6402382d3d 100644 --- a/packages/legacy/core/App/index.ts +++ b/packages/legacy/core/App/index.ts @@ -53,6 +53,7 @@ import { CredentialListFooterProps } from './types/credential-list-footer' import InactivityWrapper, { AutoLockTime } from './components/misc/InactivityWrapper' import { OpenIDCredentialRecordProvider } from './modules/openid/context/OpenIDCredentialRecordProvider' import { defaultConfig } from './container-impl' +import useInitializeAgent from './hooks/initialize-agent' export * from './navigators' export * from './services/storage' @@ -77,7 +78,7 @@ export { createStyles } from './screens/OnboardingPages' export { statusBarStyleForColor, StatusBarStyles } from './utils/luminance' export { BifoldError } from './types/error' export { EventTypes } from './constants' -export { didMigrateToAskar, migrateToAskar } from './utils/migration' +export { migrateToAskar } from './utils/migration' export { createLinkSecretIfRequired, getAgentModules } from './utils/agent' export { removeExistingInvitationIfRequired, connectFromScanOrDeepLink } from './utils/helpers' @@ -161,6 +162,7 @@ export { OpenIDCredentialRecordProvider, NotificationListItem, useDefaultStackOptions, + useInitializeAgent, Splash, Developer, Terms, diff --git a/packages/legacy/core/App/screens/Splash.tsx b/packages/legacy/core/App/screens/Splash.tsx index ca54794310..731ae03ad2 100644 --- a/packages/legacy/core/App/screens/Splash.tsx +++ b/packages/legacy/core/App/screens/Splash.tsx @@ -1,28 +1,19 @@ -import { Agent, HttpOutboundTransport, WsOutboundTransport, WalletError } from '@credo-ts/core' -import { IndyVdrPoolService } from '@credo-ts/indy-vdr/build/pool' -import { useAgent } from '@credo-ts/react-hooks' -import { agentDependencies } from '@credo-ts/react-native' -import { GetCredentialDefinitionRequest, GetSchemaRequest } from '@hyperledger/indy-vdr-shared' +import { RemoteOCABundleResolver } from '@hyperledger/aries-oca/build/legacy' import { useNavigation, CommonActions } from '@react-navigation/native' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { DeviceEventEmitter, StyleSheet } from 'react-native' -import { Config } from 'react-native-config' -import { CachesDirectoryPath } from 'react-native-fs' import { SafeAreaView } from 'react-native-safe-area-context' import { EventTypes } from '../constants' import { TOKENS, useServices } from '../container-api' import { useAnimatedComponents } from '../contexts/animated-components' -import { useAuth } from '../contexts/auth' import { DispatchAction } from '../contexts/reducers/store' import { useStore } from '../contexts/store' import { useTheme } from '../contexts/theme' +import useInitializeAgent from '../hooks/initialize-agent' import { BifoldError } from '../types/error' import { Screens, Stacks } from '../types/navigators' -import { getAgentModules, createLinkSecretIfRequired } from '../utils/agent' -import { migrateToAskar, didMigrateToAskar } from '../utils/migration' -import { RemoteOCABundleResolver } from '@hyperledger/aries-oca/build/legacy' const OnboardingVersion = 1 @@ -89,34 +80,28 @@ const resumeOnboardingAt = ( * of this view. */ const Splash: React.FC = () => { - const { agent, setAgent } = useAgent() const { t } = useTranslation() const [store, dispatch] = useStore() const navigation = useNavigation() - const { walletSecret } = useAuth() const { ColorPallet } = useTheme() const { LoadingIndicator } = useAnimatedComponents() const [mounted, setMounted] = useState(false) const initializing = useRef(false) + const { initializeAgent } = useInitializeAgent() const [ - cacheSchemas, - cacheCredDefs, { version: TermsVersion }, logger, - indyLedgers, { showPreface, enablePushNotifications }, ocaBundleResolver, historyEnabled, ] = useServices([ - TOKENS.CACHE_SCHEMAS, - TOKENS.CACHE_CRED_DEFS, TOKENS.SCREEN_TERMS, TOKENS.UTIL_LOGGER, - TOKENS.UTIL_LEDGERS, TOKENS.CONFIG, TOKENS.UTIL_OCA_RESOLVER, TOKENS.HISTORY_ENABLED, ]) + const styles = StyleSheet.create({ container: { flex: 1, @@ -137,101 +122,96 @@ const Splash: React.FC = () => { return } - const initOnboarding = async (): Promise => { - try { - if (store.onboarding.onboardingVersion !== OnboardingVersion) { - dispatch({ type: DispatchAction.ONBOARDING_VERSION, payload: [OnboardingVersion] }) - return - } + try { + if (store.onboarding.onboardingVersion !== OnboardingVersion) { + dispatch({ type: DispatchAction.ONBOARDING_VERSION, payload: [OnboardingVersion] }) + return + } - if ( - !onboardingComplete( - store.onboarding.onboardingVersion, - store.onboarding.didCompleteOnboarding, - store.onboarding.didConsiderBiometry - ) - ) { - // If onboarding was interrupted we need to pickup from where we left off. - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [ - { - name: resumeOnboardingAt( - store.onboarding.didSeePreface, - store.onboarding.didCompleteTutorial, - store.onboarding.didAgreeToTerms, - store.onboarding.didCreatePIN, - store.onboarding.didNameWallet, - store.onboarding.didConsiderBiometry, - TermsVersion, - store.preferences.enableWalletNaming, - showPreface - ), - }, - ], - }) - ) - return - } + if ( + !onboardingComplete( + store.onboarding.onboardingVersion, + store.onboarding.didCompleteOnboarding, + store.onboarding.didConsiderBiometry + ) + ) { + // If onboarding was interrupted we need to pickup from where we left off. + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [ + { + name: resumeOnboardingAt( + store.onboarding.didSeePreface, + store.onboarding.didCompleteTutorial, + store.onboarding.didAgreeToTerms, + store.onboarding.didCreatePIN, + store.onboarding.didNameWallet, + store.onboarding.didConsiderBiometry, + TermsVersion, + store.preferences.enableWalletNaming, + showPreface + ), + }, + ], + }) + ) + return + } - if (store.onboarding.onboardingVersion !== OnboardingVersion) { - dispatch({ type: DispatchAction.ONBOARDING_VERSION, payload: [OnboardingVersion] }) - return - } + if (store.onboarding.onboardingVersion !== OnboardingVersion) { + dispatch({ type: DispatchAction.ONBOARDING_VERSION, payload: [OnboardingVersion] }) + return + } - // if they previously completed onboarding before wallet naming was enabled, mark complete - if (!store.onboarding.didNameWallet) { - dispatch({ type: DispatchAction.DID_NAME_WALLET, payload: [true] }) - return - } + // if they previously completed onboarding before wallet naming was enabled, mark complete + if (!store.onboarding.didNameWallet) { + dispatch({ type: DispatchAction.DID_NAME_WALLET, payload: [true] }) + return + } - // if they previously completed onboarding before preface was enabled, mark seen - if (!store.onboarding.didSeePreface) { - dispatch({ type: DispatchAction.DID_SEE_PREFACE }) - return - } + // if they previously completed onboarding before preface was enabled, mark seen + if (!store.onboarding.didSeePreface) { + dispatch({ type: DispatchAction.DID_SEE_PREFACE }) + return + } - // add post authentication screens - const postAuthScreens = [] - if (store.onboarding.didAgreeToTerms !== TermsVersion) { - postAuthScreens.push(Screens.Terms) - } - if (!store.onboarding.didConsiderPushNotifications && enablePushNotifications) { - postAuthScreens.push(Screens.UsePushNotifications) - } - dispatch({ type: DispatchAction.SET_POST_AUTH_SCREENS, payload: [postAuthScreens] }) + // add post authentication screens + const postAuthScreens = [] + if (store.onboarding.didAgreeToTerms !== TermsVersion) { + postAuthScreens.push(Screens.Terms) + } + if (!store.onboarding.didConsiderPushNotifications && enablePushNotifications) { + postAuthScreens.push(Screens.UsePushNotifications) + } + dispatch({ type: DispatchAction.SET_POST_AUTH_SCREENS, payload: [postAuthScreens] }) - if (!store.loginAttempt.lockoutDate) { - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: Screens.EnterPIN }], - }) - ) - } else { - // return to lockout screen if lockout date is set - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: Screens.AttemptLockout }], - }) - ) - } - return - } catch (err: unknown) { - const error = new BifoldError( - t('Error.Title1044'), - t('Error.Message1044'), - (err as Error)?.message ?? err, - 1044 + if (!store.loginAttempt.lockoutDate) { + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: Screens.EnterPIN }], + }) + ) + } else { + // return to lockout screen if lockout date is set + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: Screens.AttemptLockout }], + }) ) - DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) - logger.error((err as Error)?.message ?? err) } + } catch (err: unknown) { + const error = new BifoldError( + t('Error.Title1044'), + t('Error.Message1044'), + (err as Error)?.message ?? err, + 1044 + ) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + logger.error((err as Error)?.message ?? err) } - - initOnboarding() }, [ mounted, store.authentication.didAuthenticate, @@ -257,123 +237,28 @@ const Splash: React.FC = () => { ]) useEffect(() => { - const initAgent = async (): Promise => { + const initAgentAsyncEffect = async (): Promise => { try { if ( !mounted || initializing.current || !store.authentication.didAuthenticate || - !store.onboarding.didConsiderBiometry || - !walletSecret?.id || - !walletSecret.key + !store.onboarding.didConsiderBiometry ) { return } + initializing.current = true await (ocaBundleResolver as RemoteOCABundleResolver).checkForUpdates?.() - if (agent) { - logger.info('Agent already initialized, restarting...') - - try { - await agent.wallet.open({ - id: walletSecret.id, - key: walletSecret.key, - }) - - logger.info('Opened agent wallet') - } catch (error: unknown) { - // Credo does not use error codes but this will be in the - // the error message if the wallet is already open. - const catchPhrase = 'instance already opened' - - if (error instanceof WalletError && error.message.includes(catchPhrase)) { - logger.warn('Wallet already open, nothing to do') - } else { - logger.error('Error opening existing wallet:', error as Error) - - throw new BifoldError( - 'Wallet Service', - 'There was a problem unlocking the wallet.', - (error as Error).message, - 1047 - ) - } - } - - await agent.mediationRecipient.initiateMessagePickup() - - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: Stacks.TabStack }], - }) - ) + const newAgent = await initializeAgent() + if (!newAgent) { + initializing.current = false return } - logger.info('No agent initialized, creating a new one') - - const newAgent = new Agent({ - config: { - label: store.preferences.walletName || 'Aries Bifold', - walletConfig: { - id: walletSecret.id, - key: walletSecret.key, - }, - logger, - autoUpdateStorageOnStartup: true, - }, - dependencies: agentDependencies, - modules: getAgentModules({ - indyNetworks: indyLedgers, - mediatorInvitationUrl: Config.MEDIATOR_URL, - txnCache: { - capacity: 1000, - expiryOffsetMs: 1000 * 60 * 60 * 24 * 7, - path: CachesDirectoryPath + '/txn-cache', - }, - }), - }) - const wsTransport = new WsOutboundTransport() - const httpTransport = new HttpOutboundTransport() - - newAgent.registerOutboundTransport(wsTransport) - newAgent.registerOutboundTransport(httpTransport) - - // If we haven't migrated to Aries Askar yet, we need to do this before we initialize the agent. - if (!didMigrateToAskar(store.migration)) { - newAgent.config.logger.debug('Agent not updated to Aries Askar, updating...') - - await migrateToAskar(walletSecret.id, walletSecret.key, newAgent) - - newAgent.config.logger.debug('Successfully finished updating agent to Aries Askar') - // Store that we migrated to askar. - dispatch({ - type: DispatchAction.DID_MIGRATE_TO_ASKAR, - }) - } - - await newAgent.initialize() - - await createLinkSecretIfRequired(newAgent) - - const poolService = newAgent.dependencyManager.resolve(IndyVdrPoolService) - cacheCredDefs.forEach(async ({ did, id }) => { - const pool = await poolService.getPoolForDid(newAgent.context, did) - const credDefRequest = new GetCredentialDefinitionRequest({ credentialDefinitionId: id }) - await pool.pool.submitRequest(credDefRequest) - }) - - cacheSchemas.forEach(async ({ did, id }) => { - const pool = await poolService.getPoolForDid(newAgent.context, did) - const schemaRequest = new GetSchemaRequest({ schemaId: id }) - await pool.pool.submitRequest(schemaRequest) - }) - - setAgent(newAgent) navigation.dispatch( CommonActions.reset({ index: 0, @@ -392,24 +277,15 @@ const Splash: React.FC = () => { } } - initAgent() + initAgentAsyncEffect() }, [ + initializeAgent, mounted, - agent, store.authentication.didAuthenticate, store.onboarding.didConsiderBiometry, - walletSecret, store.onboarding.postAuthScreens.length, ocaBundleResolver, - indyLedgers, - store.preferences.walletName, logger, - store.migration, - dispatch, - cacheCredDefs, - cacheSchemas, - setAgent, - store.preferences.usePushNotifications, navigation, t, ]) diff --git a/packages/legacy/core/App/utils/migration.ts b/packages/legacy/core/App/utils/migration.ts index ca0db3c450..f3c061ca28 100644 --- a/packages/legacy/core/App/utils/migration.ts +++ b/packages/legacy/core/App/utils/migration.ts @@ -6,10 +6,6 @@ import { ariesAskar } from '@hyperledger/aries-askar-react-native' import { Platform } from 'react-native' import * as RNFS from 'react-native-fs' -import { Migration as MigrationState } from '../types/state' - -export const didMigrateToAskar = (state: MigrationState) => state.didMigrateToAskar - export const migrateToAskar = async (walletId: string, key: string, agent?: Agent) => { // The backup file is kept in case anything goes wrong. this will allow us to release patches and still update the // original indy-sdk database in a future version we could manually add a check to remove the old file from storage.