diff --git a/src/App.js b/src/App.tsx similarity index 62% rename from src/App.js rename to src/App.tsx index 07948f7af95..b32cb85b7ea 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,7 +1,8 @@ import './languages'; import * as Sentry from '@sentry/react-native'; -import React, { Component } from 'react'; -import { AppRegistry, AppState, Dimensions, InteractionManager, Linking, LogBox, View } from 'react-native'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { AppRegistry, AppState, AppStateStatus, Dimensions, InteractionManager, Linking, LogBox, View } from 'react-native'; +import branch from 'react-native-branch'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -14,18 +15,13 @@ import { OfflineToast } from './components/toasts'; import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from './config/debug'; import monitorNetwork from './debugging/network'; import { Playground } from './design-system/playground/Playground'; -import { TransactionType } from './entities'; -import appEvents from './handlers/appEvents'; import handleDeeplink from './handlers/deeplinks'; import { runWalletBackupStatusChecks } from './handlers/walletReadyEvents'; -import { getIsHardhatConnected, isL2Network } from './handlers/web3'; import RainbowContextWrapper from './helpers/RainbowContext'; import isTestFlight from './helpers/isTestFlight'; -import networkTypes from './helpers/networkTypes'; import * as keychain from '@/model/keychain'; import { loadAddress } from './model/wallet'; import { Navigation } from './navigation'; -// eslint-disable-next-line import/no-unresolved import RoutesComponent from './navigation/Routes'; import { PerformanceContextMap } from './performance/PerformanceContextMap'; import { PerformanceTracking } from './performance/tracking'; @@ -33,13 +29,11 @@ import { PerformanceMetrics } from './performance/tracking/types/PerformanceMetr import { PersistQueryClientProvider, persistOptions, queryClient } from './react-query'; import store from './redux/store'; import { walletConnectLoadState } from './redux/walletconnect'; -import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; import { MainThemeProvider } from './theme/ThemeContext'; -import { ethereumUtils } from './utils'; import { branchListener } from './utils/branch'; import { addressKey } from './utils/keychainConstants'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; -import { InitialRouteContext } from '@/navigation/initialRoute'; +import { InitialRoute, InitialRouteContext } from '@/navigation/initialRoute'; import Routes from '@/navigation/routesNames'; import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; @@ -50,11 +44,13 @@ import * as ls from '@/storage'; import { migrate } from '@/migrations'; import { initListeners as initWalletConnectListeners } from '@/walletConnect'; import { saveFCMToken } from '@/notifications/tokens'; -import branch from 'react-native-branch'; import { initializeReservoirClient } from '@/resources/reservoir/client'; import { ReviewPromptAction } from '@/storage/schema'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { initializeRemoteConfig } from '@/model/remoteConfig'; +import { NavigationContainerRef } from '@react-navigation/native'; +import { RootStackParamList } from './navigation/types'; +import { Address } from 'viem'; import { IS_DEV } from './env'; import { checkIdentifierOnLaunch } from './model/backup'; @@ -67,84 +63,47 @@ enableScreens(); const containerStyle = { flex: 1 }; -class OldApp extends Component { - state = { - appState: AppState.currentState, - initialRoute: null, - eventSubscription: null, - }; - - /** - * There's a race condition in Branch's RN SDK. From a cold start, Branch - * doesn't always handle an initial URL, so we need to check for it here and - * then pass it to Branch to do its thing. - * - * @see https://github.com/BranchMetrics/react-native-branch-deep-linking-attribution/issues/673#issuecomment-1220974483 - */ - async setupDeeplinking() { +interface AppProps { + walletReady: boolean; +} + +function App({ walletReady }: AppProps) { + const [appState, setAppState] = useState(AppState.currentState); + const [initialRoute, setInitialRoute] = useState(null); + const eventSubscription = useRef | null>(null); + const branchListenerRef = useRef | null>(null); + const navigatorRef = useRef | null>(null); + + const setupDeeplinking = useCallback(async () => { const initialUrl = await Linking.getInitialURL(); - // main Branch handler - this.branchListener = await branchListener(url => { + branchListenerRef.current = await branchListener(url => { logger.debug(`Branch: listener called`, {}, logger.DebugContext.deeplinks); - try { - handleDeeplink(url, this.state.initialRoute); - } catch (e) { - logger.error(new RainbowError('Error opening deeplink'), { - message: e.message, - url, - }); + handleDeeplink(url, initialRoute); + } catch (error) { + if (error instanceof Error) { + logger.error(new RainbowError('Error opening deeplink'), { + message: error.message, + url, + }); + } else { + logger.error(new RainbowError('Error opening deeplink'), { + message: 'Unknown error', + url, + }); + } } }); - // if we have an initial URL, pass it to Branch if (initialUrl) { logger.debug(`App: has initial URL, opening with Branch`, { initialUrl }); branch.openURL(initialUrl); } - } - - async componentDidMount() { - if (!__DEV__ && isTestFlight) { - logger.info(`Test flight usage - ${isTestFlight}`); - } - - this.identifyFlow(); - const eventSub = AppState?.addEventListener('change', this?.handleAppStateChange); - this.setState({ eventSubscription: eventSub }); - appEvents.on('transactionConfirmed', this.handleTransactionConfirmed); - - const p1 = analyticsV2.initializeRudderstack(); - const p2 = this.setupDeeplinking(); - const p3 = saveFCMToken(); - await Promise.all([p1, p2, p3]); - - /** - * Needs to be called AFTER FCM token is loaded - */ - initWalletConnectListeners(); - - PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); - analyticsV2.track(analyticsV2.event.applicationDidMount); - } - - componentDidUpdate(prevProps) { - if (!prevProps.walletReady && this.props.walletReady) { - // Everything we need to do after the wallet is ready goes here - logger.info('✅ Wallet ready!'); - runWalletBackupStatusChecks(); - } - } - - componentWillUnmount() { - this.state.eventSubscription.remove(); - this.branchListener(); - } + }, [initialRoute]); - identifyFlow = async () => { + const identifyFlow = useCallback(async () => { const address = await loadAddress(); - if (address) { setTimeout(() => { InteractionManager.runAfterInteractions(() => { @@ -152,94 +111,84 @@ class OldApp extends Component { }); }, 10_000); - checkIdentifierOnLaunch(); + InteractionManager.runAfterInteractions(checkIdentifierOnLaunch); } - const initialRoute = address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN; - this.setState({ initialRoute }); - PerformanceContextMap.set('initialRoute', initialRoute); - }; - - handleAppStateChange = async nextAppState => { - // Restore WC connectors when going from BG => FG - if (this.state.appState === 'background' && nextAppState === 'active') { - store.dispatch(walletConnectLoadState()); - } - this.setState({ appState: nextAppState }); + setInitialRoute(address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + PerformanceContextMap.set('initialRoute', address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + }, []); - analyticsV2.track(analyticsV2.event.appStateChange, { - category: 'app state', - label: nextAppState, - }); - }; + const handleAppStateChange = useCallback( + (nextAppState: AppStateStatus) => { + if (appState === 'background' && nextAppState === 'active') { + store.dispatch(walletConnectLoadState()); + } + setAppState(nextAppState); + analyticsV2.track(analyticsV2.event.appStateChange, { + category: 'app state', + label: nextAppState, + }); + }, + [appState] + ); - handleNavigatorRef = navigatorRef => { - this.navigatorRef = navigatorRef; - Navigation.setTopLevelNavigator(navigatorRef); - }; + const handleNavigatorRef = useCallback((ref: NavigationContainerRef) => { + navigatorRef.current = ref; + Navigation.setTopLevelNavigator(ref); + }, []); - handleTransactionConfirmed = tx => { - const network = tx.chainId ? ethereumUtils.getNetworkFromChainId(tx.chainId) : tx.network || networkTypes.mainnet; - const isL2 = isL2Network(network); + useEffect(() => { + if (!__DEV__ && isTestFlight) { + logger.info(`Test flight usage - ${isTestFlight}`); + } + identifyFlow(); + eventSubscription.current = AppState.addEventListener('change', handleAppStateChange); - const connectedToHardhat = getIsHardhatConnected(); + const p1 = analyticsV2.initializeRudderstack(); + const p2 = setupDeeplinking(); + const p3 = saveFCMToken(); + Promise.all([p1, p2, p3]).then(() => { + initWalletConnectListeners(); + PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); + analyticsV2.track(analyticsV2.event.applicationDidMount); + }); - const updateBalancesAfter = (timeout, isL2, network) => { - const { accountAddress, nativeCurrency } = store.getState().settings; - setTimeout(() => { - logger.debug('Reloading balances for network', network); - if (isL2) { - if (tx.internalType !== TransactionType.authorize) { - // for swaps, we don't want to trigger update balances on unlock txs - queryClient.invalidateQueries({ - queryKey: userAssetsQueryKey({ - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }), - }); - } - } else { - queryClient.invalidateQueries({ - queryKey: userAssetsQueryKey({ - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }), - }); - } - }, timeout); + return () => { + eventSubscription.current?.remove(); + branchListenerRef.current?.(); }; - logger.debug('reloading balances soon...'); - updateBalancesAfter(2000, isL2, network); - updateBalancesAfter(isL2 ? 10000 : 5000, isL2, network); - }; - - render() { - return ( - - - {this.state.initialRoute && ( - - - - - )} - - - - - ); - } + }, [handleAppStateChange, identifyFlow, setupDeeplinking]); + + useEffect(() => { + if (walletReady) { + logger.info('✅ Wallet ready!'); + runWalletBackupStatusChecks(); + } + }, [walletReady]); + + return ( + + + {initialRoute && ( + + + + + )} + + + + + ); } -const OldAppWithRedux = connect(state => ({ - walletReady: state.appState.walletReady, -}))(OldApp); +export type AppStore = typeof store; +export type RootState = ReturnType; +export type AppDispatch = AppStore['dispatch']; -function App() { - return ; -} +const AppWithRedux = connect(state => ({ + walletReady: state.appState.walletReady, +}))(App); function Root() { const [initializing, setInitializing] = React.useState(true); @@ -253,7 +202,7 @@ function Root() { const [deviceId, deviceIdWasJustCreated] = await getOrCreateDeviceId(); const currentWalletAddress = await keychain.loadString(addressKey); const currentWalletAddressHash = - typeof currentWalletAddress === 'string' ? securelyHashWalletAddress(currentWalletAddress) : undefined; + typeof currentWalletAddress === 'string' ? securelyHashWalletAddress(currentWalletAddress as Address) : undefined; Sentry.setUser({ id: deviceId, @@ -336,6 +285,7 @@ function Root() { }, [setInitializing]); return initializing ? null : ( + // @ts-expect-error - Property 'children' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes> & Readonly<...>' @@ -345,7 +295,7 @@ function Root() { - + @@ -362,6 +312,7 @@ function Root() { const RootWithSentry = Sentry.wrap(Root); const PlaygroundWithReduxStore = () => ( + // @ts-expect-error - Property 'children' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes> & Readonly<...>' diff --git a/src/entities/index.ts b/src/entities/index.ts index a663754fe93..e88fedea170 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -47,6 +47,7 @@ export type { RainbowTransaction, ZerionTransaction, ZerionTransactionChange, + transactionTypes, } from './transactions'; export { GasFeeTypes, TransactionDirection, TransactionDirections, TransactionStatus, TransactionStatusTypes } from './transactions'; export type { EthereumAddress } from './wallet'; diff --git a/src/entities/transactions/index.ts b/src/entities/transactions/index.ts index 4c08517b382..89a71dc3386 100644 --- a/src/entities/transactions/index.ts +++ b/src/entities/transactions/index.ts @@ -1,5 +1,6 @@ export type { NewTransaction, NewTransactionOrAddCashTransaction, RainbowTransaction } from './transaction'; export { default as TransactionStatusTypes, TransactionStatus } from './transactionStatus'; +export { transactionTypes } from './transactionType'; export type { ZerionTransaction, ZerionTransactionChange } from './zerionTransaction'; export { default as TransactionDirections, TransactionDirection } from './transactionDirection'; diff --git a/src/graphql/config.js b/src/graphql/config.js index 8b809049e5b..ec58c6bd808 100644 --- a/src/graphql/config.js +++ b/src/graphql/config.js @@ -37,4 +37,4 @@ exports.config = { headers: {}, }, }, -}; +}; \ No newline at end of file diff --git a/src/navigation/Routes.tsx b/src/navigation/Routes.tsx new file mode 100644 index 00000000000..5ef779dc6de --- /dev/null +++ b/src/navigation/Routes.tsx @@ -0,0 +1,6 @@ +import { Platform } from 'react-native'; + +export default Platform.select({ + ios: require('./Routes.ios'), + android: require('./Routes.android'), +}); diff --git a/src/navigation/initialRoute.js b/src/navigation/initialRoute.js deleted file mode 100644 index 877868af2a6..00000000000 --- a/src/navigation/initialRoute.js +++ /dev/null @@ -1,2 +0,0 @@ -import { createContext } from 'react'; -export const InitialRouteContext = createContext(null); diff --git a/src/navigation/initialRoute.ts b/src/navigation/initialRoute.ts new file mode 100644 index 00000000000..79ec5f5e3c4 --- /dev/null +++ b/src/navigation/initialRoute.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import Routes from './routesNames'; + +export type InitialRoute = typeof Routes.WELCOME_SCREEN | typeof Routes.SWIPE_LAYOUT | null; + +export const InitialRouteContext = createContext(null); diff --git a/src/redux/appState.ts b/src/redux/appState.ts index 66094db7556..0197e288a74 100644 --- a/src/redux/appState.ts +++ b/src/redux/appState.ts @@ -11,7 +11,7 @@ const APP_STATE_UPDATE = 'contacts/APP_STATE_UPDATE'; * is called `appState`, matching the pattern used by other reducers makes * this interface `AppStateState` :). */ -interface AppStateState { +export interface AppStateState { /** * Whether or not the user's wallet has loaded. */ diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 3448d9f3ed7..2983aa51317 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -35,7 +35,7 @@ export type Account = { totalTokens: number; }; -export const enum ReviewPromptAction { +export enum ReviewPromptAction { UserPrompt = 'UserPrompt', // this is a special action that we use if the user manually prompts for review TimesLaunchedSinceInstall = 'TimesLaunchedSinceInstall', SuccessfulFiatToCryptoPurchase = 'SuccessfulFiatToCryptoPurchase', diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx index 43b4b059f1e..29d77d0be33 100644 --- a/src/theme/ThemeContext.tsx +++ b/src/theme/ThemeContext.tsx @@ -37,7 +37,7 @@ export const ThemeContext = createContext({ const { RNThemeModule } = NativeModules; -export const MainThemeProvider = (props: PropsWithChildren>) => { +export const MainThemeProvider = (props: PropsWithChildren) => { const [colorScheme, setColorScheme] = useState(null); // looks like one works on Android and another one on iOS. good. const isSystemDarkModeIOS = useDarkMode();