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

Convert App.js => App.tsx #5792

Merged
merged 17 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
261 changes: 106 additions & 155 deletions src/App.js → src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,32 +15,25 @@ 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';
import { PerformanceMetrics } from './performance/tracking/types/PerformanceMetrics';
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';
Expand All @@ -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';

Expand All @@ -67,179 +63,132 @@ 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<InitialRoute>(null);
const eventSubscription = useRef<ReturnType<typeof AppState.addEventListener> | null>(null);
const branchListenerRef = useRef<ReturnType<typeof branch.subscribe> | null>(null);
const navigatorRef = useRef<NavigationContainerRef<RootStackParamList> | 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(() => {
handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall);
});
}, 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<RootStackParamList>) => {
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 (
<Portal>
<View style={containerStyle}>
{this.state.initialRoute && (
<InitialRouteContext.Provider value={this.state.initialRoute}>
<RoutesComponent ref={this.handleNavigatorRef} />
<PortalConsumer />
</InitialRouteContext.Provider>
)}
<OfflineToast />
</View>
<NotificationsHandler walletReady={this.props.walletReady} />
</Portal>
);
}
}, [handleAppStateChange, identifyFlow, setupDeeplinking]);

useEffect(() => {
if (walletReady) {
logger.info('✅ Wallet ready!');
runWalletBackupStatusChecks();
}
}, [walletReady]);

return (
<Portal>
<View style={containerStyle}>
{initialRoute && (
<InitialRouteContext.Provider value={initialRoute}>
<RoutesComponent ref={handleNavigatorRef} />
<PortalConsumer />
</InitialRouteContext.Provider>
)}
<OfflineToast />
</View>
<NotificationsHandler walletReady={walletReady} />
</Portal>
);
}

const OldAppWithRedux = connect(state => ({
walletReady: state.appState.walletReady,
}))(OldApp);
export type AppStore = typeof store;
export type RootState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];

function App() {
return <OldAppWithRedux />;
}
const AppWithRedux = connect<unknown, AppDispatch, unknown, RootState>(state => ({
walletReady: state.appState.walletReady,
}))(App);

function Root() {
const [initializing, setInitializing] = React.useState(true);
Expand All @@ -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,
Expand Down Expand Up @@ -336,6 +285,7 @@ function Root() {
}, [setInitializing]);

return initializing ? null : (
// @ts-expect-error - Property 'children' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Provider<AppStateUpdateAction | ChartsUpdateAction | ContactsAction | ... 13 more ... | WalletsAction>> & Readonly<...>'
<ReduxProvider store={store}>
<RecoilRoot>
<PersistQueryClientProvider client={queryClient} persistOptions={persistOptions}>
Expand All @@ -345,7 +295,7 @@ function Root() {
<RainbowContextWrapper>
<SharedValuesProvider>
<ErrorBoundary>
<App />
<AppWithRedux walletReady={false} />
</ErrorBoundary>
</SharedValuesProvider>
</RainbowContextWrapper>
Expand All @@ -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<Provider<AppStateUpdateAction | ChartsUpdateAction | ContactsAction | ... 13 more ... | WalletsAction>> & Readonly<...>'
<ReduxProvider store={store}>
<Playground />
</ReduxProvider>
Expand Down
1 change: 1 addition & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions src/entities/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/graphql/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ exports.config = {
headers: {},
},
},
};
};
6 changes: 6 additions & 0 deletions src/navigation/Routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Platform } from 'react-native';

export default Platform.select({
ios: require('./Routes.ios'),
android: require('./Routes.android'),
});
Loading
Loading