diff --git a/native/crash.react.js b/native/crash.react.js index b1ea0c46b..15168c9b1 100644 --- a/native/crash.react.js +++ b/native/crash.react.js @@ -1,296 +1,296 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import Clipboard from '@react-native-clipboard/clipboard'; import invariant from 'invariant'; import _shuffle from 'lodash/fp/shuffle.js'; import * as React from 'react'; import { View, Text, Platform, StyleSheet, ScrollView, ActivityIndicator, } from 'react-native'; -import ExitApp from 'react-native-exit-app'; import { sendReportActionTypes, sendReport, } from 'lib/actions/report-actions.js'; import { logOutActionTypes, logOut } from 'lib/actions/user-actions.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import { type ErrorData, type ClientReportCreationRequest, type ReportCreationResponse, reportTypes, } from 'lib/types/report-types.js'; import type { PreRequestUserState } from 'lib/types/session-types.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import { useIsReportEnabled } from 'lib/utils/report-utils.js'; import { sanitizeReduxReport, type ReduxCrashReport, } from 'lib/utils/sanitization.js'; import sleep from 'lib/utils/sleep.js'; import Button from './components/button.react.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; +import { commCoreModule } from './native-modules.js'; import { persistConfig, codeVersion } from './redux/persist.js'; import { useSelector } from './redux/redux-utils.js'; import { wipeAndExit } from './utils/crash-utils.js'; const errorTitles = ['Oh no!!', 'Womp womp womp...']; type BaseProps = { +errorData: $ReadOnlyArray, }; type Props = { ...BaseProps, // Redux state +preRequestUserState: PreRequestUserState, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +sendReport: ( request: ClientReportCreationRequest, ) => Promise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, +crashReportingEnabled: boolean, }; type State = { +errorReportID: ?string, +doneWaiting: boolean, }; class Crash extends React.PureComponent { errorTitle = _shuffle(errorTitles)[0]; constructor(props) { super(props); this.state = { errorReportID: null, doneWaiting: !props.crashReportingEnabled, }; } componentDidMount() { if (this.state.doneWaiting) { return; } this.props.dispatchActionPromise(sendReportActionTypes, this.sendReport()); this.timeOut(); } async timeOut() { // If it takes more than 10s, give up and let the user exit await sleep(10000); this.setState({ doneWaiting: true }); } render() { const errorText = [...this.props.errorData] .reverse() .map(errorData => errorData.error.message) .join('\n'); let crashID; if (!this.state.doneWaiting) { crashID = ; } else if (this.state.doneWaiting && this.state.errorReportID) { crashID = ( Crash report ID: {this.state.errorReportID} ); } else { crashID = ( Crash reporting can be enabled in the Profile tab. ); } const buttonStyle = { opacity: Number(this.state.doneWaiting) }; return ( {this.errorTitle} I'm sorry, but the app crashed. {crashID} Here's some text that's probably not helpful: {errorText} ); } async sendReport() { const sanitizedReduxReport: ReduxCrashReport = sanitizeReduxReport({ preloadedState: actionLogger.preloadedState, currentState: actionLogger.currentState, actions: actionLogger.actions, }); const result = await this.props.sendReport({ type: reportTypes.ERROR, platformDetails: { platform: Platform.OS, codeVersion, stateVersion: persistConfig.version, }, errors: this.props.errorData.map(data => ({ errorMessage: data.error.message, stack: data.error.stack, componentStack: data.info && data.info.componentStack, })), ...sanitizedReduxReport, }); this.setState({ errorReportID: result.id, doneWaiting: true, }); } onPressKill = () => { if (!this.state.doneWaiting) { return; } - ExitApp.exitApp(); + commCoreModule.terminate(); }; onPressWipe = async () => { if (!this.state.doneWaiting) { return; } this.props.dispatchActionPromise(logOutActionTypes, this.logOutAndExit()); }; async logOutAndExit() { try { await this.props.logOut(this.props.preRequestUserState); } catch (e) {} await wipeAndExit(); } onCopyCrashReportID = () => { invariant(this.state.errorReportID, 'should be set'); Clipboard.setString(this.state.errorReportID); }; } const styles = StyleSheet.create({ button: { backgroundColor: '#FF0000', borderRadius: 5, marginHorizontal: 10, paddingHorizontal: 10, paddingVertical: 5, }, buttonText: { color: 'white', fontSize: 16, }, buttons: { flexDirection: 'row', }, container: { alignItems: 'center', backgroundColor: 'white', flex: 1, justifyContent: 'center', }, copyCrashReportIDButtonText: { color: '#036AFF', }, crashID: { flexDirection: 'row', paddingBottom: 12, paddingTop: 2, }, crashIDText: { color: 'black', paddingRight: 8, }, errorReportID: { flexDirection: 'row', height: 20, }, errorReportIDText: { color: 'black', paddingRight: 8, }, errorText: { color: 'black', fontFamily: Platform.select({ ios: 'Menlo', default: 'monospace', }), }, header: { color: 'black', fontSize: 24, paddingBottom: 24, }, scrollView: { flex: 1, marginBottom: 24, marginTop: 12, maxHeight: 200, paddingHorizontal: 50, }, text: { color: 'black', paddingBottom: 12, }, }); const ConnectedCrash: React.ComponentType = React.memo( function ConnectedCrash(props: BaseProps) { const preRequestUserState = useSelector(preRequestUserStateSelector); const dispatchActionPromise = useDispatchActionPromise(); const callSendReport = useServerCall(sendReport); const callLogOut = useServerCall(logOut); const crashReportingEnabled = useIsReportEnabled('crashReports'); return ( ); }, ); export default ConnectedCrash; diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js index 30cd1cdac..4b5109344 100644 --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -1,197 +1,196 @@ // @flow import * as React from 'react'; import { Alert } from 'react-native'; -import ExitApp from 'react-native-exit-app'; import { useDispatch } from 'react-redux'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { logInActionSources, type LogInActionSource, } from 'lib/types/account-types.js'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { convertClientDBThreadInfosToRawThreadInfos } from 'lib/utils/thread-ops-utils.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { StaffContext } from '../staff/staff-context.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function SQLiteDataHandler(): React.Node { const storeLoaded = useSelector(state => state.storeLoaded); const dispatch = useDispatch(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const cookie = useSelector(state => state.cookie); const urlPrefix = useSelector(state => state.urlPrefix); const staffCanSee = useStaffCanSee(); const { staffUserHasBeenLoggedIn } = React.useContext(StaffContext); const loggedIn = useSelector(isLoggedIn); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const callFetchNewCookieFromNativeCredentials = React.useCallback( async (source: LogInActionSource) => { try { await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, source, ); dispatch({ type: setStoreLoadedActionType }); } catch (fetchCookieException) { if (staffCanSee) { Alert.alert( `Error fetching new cookie from native credentials: ${ getMessageForException(fetchCookieException) ?? '{no exception message}' }. Please kill the app.`, ); } else { - ExitApp.exitApp(); + commCoreModule.terminate(); } } }, [cookie, dispatch, staffCanSee, urlPrefix], ); const callClearSensitiveData = React.useCallback( async (triggeredBy: string) => { if (staffCanSee || staffUserHasBeenLoggedIn) { Alert.alert('Starting SQLite database deletion process'); } await commCoreModule.clearSensitiveData(); if (staffCanSee || staffUserHasBeenLoggedIn) { Alert.alert( 'SQLite database successfully deleted', `SQLite database deletion was triggered by ${triggeredBy}`, ); } }, [staffCanSee, staffUserHasBeenLoggedIn], ); const handleSensitiveData = React.useCallback(async () => { try { const databaseCurrentUserInfoID = await commCoreModule.getCurrentUserID(); if ( databaseCurrentUserInfoID && databaseCurrentUserInfoID !== currentLoggedInUserID ) { await callClearSensitiveData('change in logged-in user credentials'); } if (currentLoggedInUserID) { await commCoreModule.setCurrentUserID(currentLoggedInUserID); } const databaseDeviceID = await commCoreModule.getDeviceID(); if (!databaseDeviceID) { await commCoreModule.setDeviceID('MOBILE'); } } catch (e) { if (isTaskCancelledError(e)) { return; } if (__DEV__) { throw e; } else { console.log(e); - ExitApp.exitApp(); + commCoreModule.terminate(); } } }, [callClearSensitiveData, currentLoggedInUserID]); React.useEffect(() => { if (!rehydrateConcluded) { return; } const databaseNeedsDeletion = commCoreModule.checkIfDatabaseNeedsDeletion(); if (databaseNeedsDeletion) { (async () => { try { await callClearSensitiveData('detecting corrupted database'); } catch (e) { if (__DEV__) { throw e; } else { console.log(e); - ExitApp.exitApp(); + commCoreModule.terminate(); } } await callFetchNewCookieFromNativeCredentials( logInActionSources.corruptedDatabaseDeletion, ); })(); return; } const sensitiveDataHandled = handleSensitiveData(); if (storeLoaded) { return; } if (!loggedIn) { dispatch({ type: setStoreLoadedActionType }); return; } (async () => { await sensitiveDataHandled; try { const { threads, messages, drafts } = await commCoreModule.getClientDBStore(); const threadInfosFromDB = convertClientDBThreadInfosToRawThreadInfos(threads); dispatch({ type: setClientDBStoreActionType, payload: { drafts, messages, threadStore: { threadInfos: threadInfosFromDB }, currentUserID: currentLoggedInUserID, }, }); } catch (setStoreException) { if (isTaskCancelledError(setStoreException)) { dispatch({ type: setStoreLoadedActionType }); return; } if (staffCanSee) { Alert.alert( `Error setting threadStore or messageStore: ${ getMessageForException(setStoreException) ?? '{no exception message}' }`, ); } await callFetchNewCookieFromNativeCredentials( logInActionSources.sqliteLoadFailure, ); } })(); }, [ currentLoggedInUserID, handleSensitiveData, loggedIn, cookie, dispatch, rehydrateConcluded, staffCanSee, storeLoaded, urlPrefix, staffUserHasBeenLoggedIn, callFetchNewCookieFromNativeCredentials, callClearSensitiveData, ]); return null; } export { SQLiteDataHandler }; diff --git a/native/profile/dev-tools.react.js b/native/profile/dev-tools.react.js index 0bded07ff..43689eaa6 100644 --- a/native/profile/dev-tools.react.js +++ b/native/profile/dev-tools.react.js @@ -1,256 +1,256 @@ // @flow import * as React from 'react'; import { View, Text, Platform } from 'react-native'; -import ExitApp from 'react-native-exit-app'; import { ScrollView } from 'react-native-gesture-handler'; import { useDispatch } from 'react-redux'; import type { Dispatch } from 'lib/types/redux-types.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import Button from '../components/button.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; +import { commCoreModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { CustomServerModalRouteName } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles, type Colors } from '../themes/colors.js'; import { wipeAndExit } from '../utils/crash-utils.js'; import { checkForMissingNatDevHostname } from '../utils/dev-hostname.js'; import { nodeServerOptions } from '../utils/url-utils.js'; const ServerIcon = () => ( ); type BaseProps = { +navigation: ProfileNavigationProp<'DevTools'>, +route: NavigationRoute<'DevTools'>, }; type Props = { ...BaseProps, +urlPrefix: string, +customServer: ?string, +colors: Colors, +styles: typeof unboundStyles, +dispatch: Dispatch, }; class DevTools extends React.PureComponent { render() { const { panelIosHighlightUnderlay: underlay } = this.props.colors; const serverButtons = []; for (const server of nodeServerOptions) { const icon = server === this.props.urlPrefix ? : null; serverButtons.push( , ); serverButtons.push( , ); } const customServerLabel = this.props.customServer ? ( {'custom: '} {this.props.customServer} ) : ( custom ); const customServerIcon = this.props.customServer === this.props.urlPrefix ? : null; serverButtons.push( , ); return ( SERVER {serverButtons} ); } onPressCrash = () => { throw new Error('User triggered crash through dev menu!'); }; onPressKill = () => { - ExitApp.exitApp(); + commCoreModule.terminate(); }; onPressWipe = async () => { await wipeAndExit(); }; onSelectServer = (server: string) => { if (server !== this.props.urlPrefix) { this.props.dispatch({ type: setURLPrefix, payload: server, }); } }; onSelectCustomServer = () => { checkForMissingNatDevHostname(); this.props.navigation.navigate(CustomServerModalRouteName, { presentedFrom: this.props.route.key, }); }; } const unboundStyles = { container: { flex: 1, }, customServerLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, hr: { backgroundColor: 'panelForegroundBorder', height: 1, marginHorizontal: 15, }, icon: { lineHeight: Platform.OS === 'ios' ? 18 : 20, }, redText: { color: 'redText', flex: 1, fontSize: 16, }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, serverContainer: { flex: 1, }, serverText: { color: 'panelForegroundLabel', fontSize: 16, }, slightlyPaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 2, }, }; const ConnectedDevTools: React.ComponentType = React.memo( function ConnectedDevTools(props: BaseProps) { const urlPrefix = useSelector(state => state.urlPrefix); const customServer = useSelector(state => state.customServer); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatch = useDispatch(); return ( ); }, ); export default ConnectedDevTools; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index d390c1b7b..71244b8ac 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,547 +1,546 @@ // @flow import { AppState as NativeAppState, Platform, Alert } from 'react-native'; -import ExitApp from 'react-native-exit-app'; import Orientation from 'react-native-orientation-locker'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { setDeviceTokenActionTypes } from 'lib/actions/device-actions.js'; import { siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, } from 'lib/actions/user-actions.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { processThreadStoreOperations } from 'lib/reducers/thread-reducer.js'; import { invalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/account-utils.js'; import { isStaff } from 'lib/shared/user-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { defaultConnectionInfo, incrementalStateSyncActionType, } from 'lib/types/socket-types.js'; import type { ThreadStoreOperation } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import { convertMessageStoreOperationsToClientDBOperations } from 'lib/utils/message-ops-utils.js'; import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils.js'; import { resetUserStateActionType, recordNotifPermissionAlertActionType, updateDimensionsActiveType, updateConnectivityActiveType, updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, } from './action-types.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { defaultDimensionsInfo } from './dimensions-updater.react.js'; import { persistConfig, setPersistor } from './persist.js'; import type { AppState } from './state-types.js'; import { commCoreModule } from '../native-modules.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import { defaultNotifPermissionAlertInfo } from '../push/alerts.js'; import reactotron from '../reactotron.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultConnectivityInfo } from '../types/connectivity.js'; import { defaultGlobalThemeInfo } from '../types/themes.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { defaultURLPrefix, natNodeServer, setCustomServer, getDevServerHostname, } from '../utils/url-utils.js'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, inconsistencyReports: [], }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: 0, }, storeLoaded: false, updatesCurrentAsOf: 0, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, cookie: null, deviceToken: null, dataLoaded: false, urlPrefix: defaultURLPrefix, customServer: natNodeServer, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, connection: defaultConnectionInfo(Platform.OS), watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultEnabledApps, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [], }, nextLocalID: 0, _persist: null, dimensions: defaultDimensionsInfo, connectivity: defaultConnectivityInfo, globalThemeInfo: defaultGlobalThemeInfo, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), frozen: false, userPolicies: {}, }: AppState); function reducer(state: AppState = defaultState, action: Action) { if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys = Object.keys(action.payload ?? {}); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( (action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === logOutActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return state; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.logInActionSource, )) || ((action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.logInActionSource, )) ) { return state; } if (action.type === setCustomServer) { return { ...state, customServer: action.payload, }; } else if (action.type === recordNotifPermissionAlertActionType) { return { ...state, notifPermissionAlertInfo: { totalAlerts: state.notifPermissionAlertInfo.totalAlerts + 1, lastAlertTime: action.payload.time, }, }; } else if (action.type === resetUserStateActionType) { const cookie = state.cookie && state.cookie.startsWith('anonymous=') ? state.cookie : null; const currentUserInfo = state.currentUserInfo && state.currentUserInfo.anonymous ? state.currentUserInfo : null; return { ...state, currentUserInfo, cookie, }; } else if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateThemeInfoActionType) { return { ...state, globalThemeInfo: { ...state.globalThemeInfo, ...action.payload, }, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setDeviceTokenActionTypes.success) { return { ...state, deviceToken: action.payload, }; } else if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; if (state.messageStore.threads[threadID]) { state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, [threadID]: { ...state.messageStore.threads[threadID], lastNavigatedTo: time, }, }, }, }; } return state; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); state = { ...state, cookie: action.payload.sessionChange.cookie, }; } else if (action.type === incrementalStateSyncActionType) { let wipeDeviceToken = false; for (const update of action.payload.updatesResult.newUpdates) { if ( update.type === updateTypes.BAD_DEVICE_TOKEN && update.deviceToken === state.deviceToken ) { wipeDeviceToken = true; break; } } if (wipeDeviceToken) { state = { ...state, deviceToken: null, }; } } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } const baseReducerResult = baseReducer(state, (action: BaseAction)); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { draftStoreOperations, threadStoreOperations, messageStoreOperations, } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const convertedThreadStoreOperations = convertThreadStoreOperationsToClientDBOperations( threadStoreOperationsWithUnreadFix, ); const convertedMessageStoreOperations = convertMessageStoreOperationsToClientDBOperations(messageStoreOperations); (async () => { try { const promises = []; if (convertedThreadStoreOperations.length > 0) { promises.push( commCoreModule.processThreadStoreOperations( convertedThreadStoreOperations, ), ); } if (convertedMessageStoreOperations.length > 0) { promises.push( commCoreModule.processMessageStoreOperations( convertedMessageStoreOperations, ), ); } if (draftStoreOperations.length > 0) { promises.push( commCoreModule.processDraftStoreOperations(draftStoreOperations), ); } await Promise.all(promises); } catch (e) { if (isTaskCancelledError(e)) { return; } - ExitApp.exitApp(); + commCoreModule.terminate(); } })(); return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how to ' + `speak to it anymore. Please use the ${app} app to update!`, [{ text: 'OK' }], { cancelable: true }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const updatedActiveThreadInfo = { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/utils/crash-utils.js b/native/utils/crash-utils.js index 2918fd3bb..1f5c54c2b 100644 --- a/native/utils/crash-utils.js +++ b/native/utils/crash-utils.js @@ -1,20 +1,20 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; -import ExitApp from 'react-native-exit-app'; import sleep from 'lib/utils/sleep.js'; +import { commCoreModule } from '../native-modules.js'; import { navStateAsyncStorageKey } from '../navigation/persistance.js'; import { getPersistor } from '../redux/persist.js'; async function wipeAndExit() { await Promise.all([ getPersistor().purge(), __DEV__ ? AsyncStorage.removeItem(navStateAsyncStorageKey) : null, ]); await sleep(50); - ExitApp.exitApp(); + commCoreModule.terminate(); } export { wipeAndExit };