Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509133
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
48 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/native/qr-code/qr-code-screen.react.js b/native/account/qr-code-screen.react.js
similarity index 100%
rename from native/qr-code/qr-code-screen.react.js
rename to native/account/qr-code-screen.react.js
diff --git a/native/qr-code/sign-in-navigator.react.js b/native/account/sign-in-navigator.react.js
similarity index 100%
rename from native/qr-code/sign-in-navigator.react.js
rename to native/account/sign-in-navigator.react.js
diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js
index 061017518..136dcd2f0 100644
--- a/native/navigation/root-navigator.react.js
+++ b/native/navigation/root-navigator.react.js
@@ -1,322 +1,322 @@
// @flow
import type {
StackNavigationState,
StackOptions,
StackNavigationEventMap,
StackNavigatorProps,
ExtraStackNavigatorProps,
ParamListBase,
StackNavigationHelpers,
StackNavigationProp,
StackRouterOptions,
RouteProp,
} from '@react-navigation/core';
import {
createNavigatorFactory,
useNavigationBuilder,
} from '@react-navigation/native';
import { StackView } from '@react-navigation/stack';
import * as React from 'react';
import { Platform } from 'react-native';
import { enableScreens } from 'react-native-screens';
import AppNavigator from './app-navigator.react.js';
import InviteLinkModal from './invite-link-modal.react.js';
import { defaultStackScreenOptions, transitionPreset } from './options.js';
import { RootNavigatorContext } from './root-navigator-context.js';
import RootRouter, {
type RootRouterExtraNavigationHelpers,
type RootRouterNavigationAction,
} from './root-router.js';
import {
LoggedOutModalRouteName,
AppRouteName,
ThreadPickerModalRouteName,
ImagePasteModalRouteName,
AddUsersModalRouteName,
CustomServerModalRouteName,
ColorSelectorModalRouteName,
ComposeSubchannelModalRouteName,
SidebarListModalRouteName,
SubchannelsListModalRouteName,
MessageReactionsModalRouteName,
type ScreenParamList,
type RootParamList,
TermsAndPrivacyRouteName,
RegistrationRouteName,
InviteLinkModalRouteName,
InviteLinkNavigatorRouteName,
CommunityCreationRouteName,
RolesNavigatorRouteName,
SignInNavigatorRouteName,
UserProfileBottomSheetNavigatorRouteName,
KeyserverSelectionBottomSheetRouteName,
ConnectFarcasterBottomSheetRouteName,
TagFarcasterChannelNavigatorRouteName,
CreateMissingSIWEBackupMessageRouteName,
RestoreSIWEBackupRouteName,
LinkedDevicesBottomSheetRouteName,
} from './route-names.js';
import LoggedOutModal from '../account/logged-out-modal.react.js';
import CreateMissingSIWEBackupMessage from '../account/registration/missing-registration-data/missing-siwe-backup-message.react.js';
import RegistrationNavigator from '../account/registration/registration-navigator.react.js';
+import SignInNavigator from '../account/sign-in-navigator.react.js';
import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react.js';
import RestoreSIWEBackup from '../backup/restore-siwe-backup.react.js';
import ThreadPickerModal from '../calendar/thread-picker-modal.react.js';
import ImagePasteModal from '../chat/image-paste-modal.react.js';
import MessageReactionsModal from '../chat/message-reactions-modal.react.js';
import AddUsersModal from '../chat/settings/add-users-modal.react.js';
import ColorSelectorModal from '../chat/settings/color-selector-modal.react.js';
import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react.js';
import SidebarListModal from '../chat/sidebar-list-modal.react.js';
import SubchannelsListModal from '../chat/subchannels-list-modal.react.js';
import CommunityCreationNavigator from '../community-creation/community-creation-navigator.react.js';
import TagFarcasterChannelNavigator from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-navigator.react.js';
import ConnectFarcasterBottomSheet from '../components/connect-farcaster-bottom-sheet.react.js';
import InviteLinksNavigator from '../invite-links/invite-links-navigator.react.js';
import CustomServerModal from '../profile/custom-server-modal.react.js';
import KeyserverSelectionBottomSheet from '../profile/keyserver-selection-bottom-sheet.react.js';
import LinkedDevicesBottomSheet from '../profile/linked-devices-bottom-sheet.react.js';
-import SignInNavigator from '../qr-code/sign-in-navigator.react.js';
import RolesNavigator from '../roles/roles-navigator.react.js';
import UserProfileBottomSheetNavigator from '../user-profile/user-profile-bottom-sheet-navigator.react.js';
enableScreens();
export type RootNavigationHelpers<ParamList: ParamListBase = ParamListBase> = {
...$Exact<StackNavigationHelpers<ParamList>>,
...RootRouterExtraNavigationHelpers,
...
};
type RootNavigatorProps = StackNavigatorProps<RootNavigationHelpers<>>;
function RootNavigator({
initialRouteName,
children,
screenOptions,
defaultScreenOptions,
screenListeners,
id,
...rest
}: RootNavigatorProps) {
const [keyboardHandlingEnabled, setKeyboardHandlingEnabled] =
React.useState(true);
const mergedScreenOptions = React.useMemo(() => {
if (typeof screenOptions === 'function') {
return (input: {
+route: RouteProp<>,
+navigation: RootNavigationHelpers<>,
}) => ({
...screenOptions(input),
keyboardHandlingEnabled,
});
}
return {
...screenOptions,
keyboardHandlingEnabled,
};
}, [screenOptions, keyboardHandlingEnabled]);
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState,
RootRouterNavigationAction,
StackOptions,
StackRouterOptions,
RootNavigationHelpers<>,
StackNavigationEventMap,
ExtraStackNavigatorProps,
>(RootRouter, {
id,
initialRouteName,
children,
screenOptions: mergedScreenOptions,
defaultScreenOptions,
screenListeners,
});
const rootNavigationContext = React.useMemo(
() => ({ setKeyboardHandlingEnabled }),
[setKeyboardHandlingEnabled],
);
return (
<RootNavigatorContext.Provider value={rootNavigationContext}>
<StackView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
detachInactiveScreens={Platform.OS !== 'ios'}
/>
</RootNavigatorContext.Provider>
);
}
const createRootNavigator = createNavigatorFactory<
StackNavigationState,
StackOptions,
StackNavigationEventMap,
RootNavigationHelpers<>,
ExtraStackNavigatorProps,
>(RootNavigator);
const defaultScreenOptions = {
...defaultStackScreenOptions,
...transitionPreset,
cardStyle: { backgroundColor: 'transparent' },
presentation: 'modal',
headerShown: false,
};
const disableGesturesScreenOptions = {
gestureEnabled: false,
};
const modalOverlayScreenOptions = {
cardOverlayEnabled: true,
presentation: 'transparentModal',
};
const termsAndPrivacyModalScreenOptions = {
gestureEnabled: false,
cardOverlayEnabled: true,
presentation: 'transparentModal',
};
export type RootRouterNavigationProp<
ParamList: ParamListBase = ParamListBase,
RouteName: $Keys<ParamList> = $Keys<ParamList>,
> = {
...StackNavigationProp<ParamList, RouteName>,
...RootRouterExtraNavigationHelpers,
};
export type RootNavigationProp<
RouteName: $Keys<ScreenParamList> = $Keys<ScreenParamList>,
> = {
...StackNavigationProp<ScreenParamList, RouteName>,
...RootRouterExtraNavigationHelpers,
};
const Root = createRootNavigator<
ScreenParamList,
RootParamList,
RootNavigationHelpers<ScreenParamList>,
>();
function RootComponent(): React.Node {
return (
<Root.Navigator screenOptions={defaultScreenOptions}>
<Root.Screen
name={LoggedOutModalRouteName}
component={LoggedOutModal}
options={disableGesturesScreenOptions}
/>
<Root.Screen
name={RegistrationRouteName}
component={RegistrationNavigator}
options={disableGesturesScreenOptions}
/>
<Root.Screen
name={SignInNavigatorRouteName}
component={SignInNavigator}
options={disableGesturesScreenOptions}
/>
<Root.Screen
name={CommunityCreationRouteName}
component={CommunityCreationNavigator}
/>
<Root.Screen name={AppRouteName} component={AppNavigator} />
<Root.Screen
name={TermsAndPrivacyRouteName}
component={TermsAndPrivacyModal}
options={termsAndPrivacyModalScreenOptions}
/>
<Root.Screen
name={InviteLinkModalRouteName}
component={InviteLinkModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={InviteLinkNavigatorRouteName}
component={InviteLinksNavigator}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ThreadPickerModalRouteName}
component={ThreadPickerModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ImagePasteModalRouteName}
component={ImagePasteModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={AddUsersModalRouteName}
component={AddUsersModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={CustomServerModalRouteName}
component={CustomServerModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ColorSelectorModalRouteName}
component={ColorSelectorModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ComposeSubchannelModalRouteName}
component={ComposeSubchannelModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={SidebarListModalRouteName}
component={SidebarListModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={SubchannelsListModalRouteName}
component={SubchannelsListModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={MessageReactionsModalRouteName}
component={MessageReactionsModal}
options={modalOverlayScreenOptions}
/>
<Root.Screen name={RolesNavigatorRouteName} component={RolesNavigator} />
<Root.Screen
name={UserProfileBottomSheetNavigatorRouteName}
component={UserProfileBottomSheetNavigator}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={KeyserverSelectionBottomSheetRouteName}
component={KeyserverSelectionBottomSheet}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={LinkedDevicesBottomSheetRouteName}
component={LinkedDevicesBottomSheet}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={ConnectFarcasterBottomSheetRouteName}
component={ConnectFarcasterBottomSheet}
options={modalOverlayScreenOptions}
/>
<Root.Screen
name={TagFarcasterChannelNavigatorRouteName}
component={TagFarcasterChannelNavigator}
/>
<Root.Screen
name={CreateMissingSIWEBackupMessageRouteName}
component={CreateMissingSIWEBackupMessage}
/>
<Root.Screen
name={RestoreSIWEBackupRouteName}
component={RestoreSIWEBackup}
/>
</Root.Navigator>
);
}
export default RootComponent;
diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js
index 6cf669ff8..fed08a791 100644
--- a/native/profile/secondary-device-qr-code-scanner.react.js
+++ b/native/profile/secondary-device-qr-code-scanner.react.js
@@ -1,455 +1,455 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import { BarCodeScanner, type BarCodeEvent } from 'expo-barcode-scanner';
import invariant from 'invariant';
import * as React from 'react';
import { View, Text } from 'react-native';
import { parseDataFromDeepLink } from 'lib/facts/links.js';
import {
getOwnPeerDevices,
getKeyserverDeviceID,
} from 'lib/selectors/user-selectors.js';
import { useDeviceListUpdate } from 'lib/shared/device-list-utils.js';
import { IdentityClientContext } from 'lib/shared/identity-client-context.js';
import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js';
import {
backupKeysValidator,
type BackupKeys,
} from 'lib/types/backup-types.js';
import {
identityDeviceTypes,
type IdentityDeviceType,
} from 'lib/types/identity-service-types.js';
import {
tunnelbrokerToDeviceMessageTypes,
type TunnelbrokerToDeviceMessage,
} from 'lib/types/tunnelbroker/messages.js';
import {
peerToPeerMessageTypes,
peerToPeerMessageValidator,
type PeerToPeerMessage,
} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js';
import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js';
import { assertWithValidator } from 'lib/utils/validation-utils.js';
import type { ProfileNavigationProp } from './profile.react.js';
import { useClientBackup } from '../backup/use-client-backup.js';
import { useGetBackupSecretForLoggedInUser } from '../backup/use-get-backup-secret.js';
import TextInput from '../components/text-input.react.js';
import { commCoreModule } from '../native-modules.js';
import HeaderRightTextButton from '../navigation/header-right-text-button.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
-import {
- composeTunnelbrokerQRAuthMessage,
- parseTunnelbrokerQRAuthMessage,
-} from '../qr-code/qr-code-utils.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles, useColors } from '../themes/colors.js';
import Alert from '../utils/alert.js';
+import {
+ composeTunnelbrokerQRAuthMessage,
+ parseTunnelbrokerQRAuthMessage,
+} from '../utils/qr-code-utils.js';
import { deviceIsEmulator } from '../utils/url-utils.js';
const barCodeTypes = [BarCodeScanner.Constants.BarCodeType.qr];
type Props = {
+navigation: ProfileNavigationProp<'SecondaryDeviceQRCodeScanner'>,
+route: NavigationRoute<'SecondaryDeviceQRCodeScanner'>,
};
// eslint-disable-next-line no-unused-vars
function SecondaryDeviceQRCodeScanner(props: Props): React.Node {
const [hasPermission, setHasPermission] = React.useState<?boolean>(null);
const [scanned, setScanned] = React.useState(false);
const [urlInput, setURLInput] = React.useState('');
const styles = useStyles(unboundStyles);
const { goBack, setOptions } = useNavigation();
const tunnelbrokerContext = useTunnelbroker();
const identityContext = React.useContext(IdentityClientContext);
invariant(identityContext, 'identity context not set');
const aes256Key = React.useRef<?string>(null);
const secondaryDeviceID = React.useRef<?string>(null);
const secondaryDeviceType = React.useRef<?IdentityDeviceType>(null);
const runDeviceListUpdate = useDeviceListUpdate();
const ownPeerDevices = useSelector(getOwnPeerDevices);
const keyserverDeviceID = getKeyserverDeviceID(ownPeerDevices);
const getBackupSecret = useGetBackupSecretForLoggedInUser();
const { retrieveLatestBackupInfo } = useClientBackup();
const { panelForegroundTertiaryLabel } = useColors();
const tunnelbrokerMessageListener = React.useCallback(
async (message: TunnelbrokerToDeviceMessage) => {
const encryptionKey = aes256Key.current;
const targetDeviceID = secondaryDeviceID.current;
if (!encryptionKey || !targetDeviceID) {
return;
}
if (message.type !== tunnelbrokerToDeviceMessageTypes.MESSAGE_TO_DEVICE) {
return;
}
let innerMessage: PeerToPeerMessage;
try {
innerMessage = JSON.parse(message.payload);
} catch {
return;
}
if (
!peerToPeerMessageValidator.is(innerMessage) ||
innerMessage.type !== peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE
) {
return;
}
const payload = await parseTunnelbrokerQRAuthMessage(
encryptionKey,
innerMessage,
);
if (
!payload ||
payload.type !==
qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS
) {
return;
}
Alert.alert('Device added', 'Device registered successfully', [
{ text: 'OK', onPress: goBack },
]);
},
[goBack],
);
React.useEffect(() => {
tunnelbrokerContext.addListener(tunnelbrokerMessageListener);
return () => {
tunnelbrokerContext.removeListener(tunnelbrokerMessageListener);
};
}, [tunnelbrokerMessageListener, tunnelbrokerContext]);
React.useEffect(() => {
void (async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === 'granted');
if (status !== 'granted') {
Alert.alert(
'No access to camera',
'Please allow Comm to access your camera in order to scan the QR code.',
[{ text: 'OK' }],
);
goBack();
}
})();
}, [goBack]);
const processDeviceListUpdate = React.useCallback(async () => {
try {
const { deviceID: primaryDeviceID, userID } =
await identityContext.getAuthMetadata();
if (!primaryDeviceID || !userID) {
throw new Error('missing auth metadata');
}
const encryptionKey = aes256Key.current;
const targetDeviceID = secondaryDeviceID.current;
if (!encryptionKey || !targetDeviceID) {
throw new Error('missing tunnelbroker message data');
}
const deviceType = secondaryDeviceType.current;
const sendDeviceListUpdateSuccessMessage = async () => {
let backupData = null;
if (deviceType !== identityDeviceTypes.KEYSERVER) {
const [backupSecret, latestBackupInfo] = await Promise.all([
getBackupSecret(),
retrieveLatestBackupInfo(),
]);
const backupKeysResponse = await commCoreModule.retrieveBackupKeys(
backupSecret,
latestBackupInfo.backupID,
);
backupData = assertWithValidator<BackupKeys>(
JSON.parse(backupKeysResponse),
backupKeysValidator,
);
}
const message = await composeTunnelbrokerQRAuthMessage(encryptionKey, {
type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS,
userID,
primaryDeviceID,
backupData,
});
await tunnelbrokerContext.sendMessageToDevice({
deviceID: targetDeviceID,
payload: JSON.stringify(message),
});
};
const handleReplaceDevice = async () => {
try {
if (!keyserverDeviceID) {
throw new Error('missing keyserver device ID');
}
await runDeviceListUpdate({
type: 'replace',
deviceIDToRemove: keyserverDeviceID,
newDeviceID: targetDeviceID,
});
await sendDeviceListUpdateSuccessMessage();
} catch (err) {
console.log('Device replacement error:', err);
Alert.alert(
'Adding device failed',
'Failed to update the device list',
[{ text: 'OK' }],
);
goBack();
}
};
if (
deviceType !== identityDeviceTypes.KEYSERVER ||
!keyserverDeviceID ||
keyserverDeviceID === targetDeviceID
) {
await runDeviceListUpdate({
type: 'add',
deviceID: targetDeviceID,
});
await sendDeviceListUpdateSuccessMessage();
return;
}
Alert.alert(
'Existing keyserver detected',
'Do you want to replace your existing keyserver with this new one?',
[
{
text: 'No',
onPress: goBack,
style: 'cancel',
},
{
text: 'Replace',
onPress: handleReplaceDevice,
style: 'destructive',
},
],
);
} catch (err) {
console.log('Primary device error:', err);
Alert.alert('Adding device failed', 'Failed to update the device list', [
{ text: 'OK' },
]);
goBack();
}
}, [
getBackupSecret,
goBack,
identityContext,
keyserverDeviceID,
retrieveLatestBackupInfo,
runDeviceListUpdate,
tunnelbrokerContext,
]);
const onPressSave = React.useCallback(async () => {
if (!urlInput) {
return;
}
const parsedData = parseDataFromDeepLink(urlInput);
const keysMatch = parsedData?.data?.keys;
if (!parsedData || !keysMatch) {
Alert.alert(
'Scan failed',
'QR code does not contain a valid pair of keys.',
[{ text: 'OK' }],
);
return;
}
try {
const keys = JSON.parse(decodeURIComponent(keysMatch));
const { aes256, ed25519 } = keys;
aes256Key.current = aes256;
secondaryDeviceID.current = ed25519;
secondaryDeviceType.current = parsedData.data.deviceType;
} catch (err) {
console.log('Failed to decode URI component:', err);
return;
}
await processDeviceListUpdate();
}, [processDeviceListUpdate, urlInput]);
const buttonDisabled = !urlInput;
React.useEffect(() => {
if (!deviceIsEmulator) {
return;
}
setOptions({
headerRight: () => (
<HeaderRightTextButton
label="Save"
onPress={onPressSave}
disabled={buttonDisabled}
/>
),
});
}, [buttonDisabled, onPressSave, setOptions]);
const onChangeText = React.useCallback(
(text: string) => setURLInput(text),
[],
);
const onConnect = React.useCallback(
async (barCodeEvent: BarCodeEvent) => {
const { data } = barCodeEvent;
const parsedData = parseDataFromDeepLink(data);
const keysMatch = parsedData?.data?.keys;
if (!parsedData || !keysMatch) {
Alert.alert(
'Scan failed',
'QR code does not contain a valid pair of keys.',
[{ text: 'OK' }],
);
return;
}
try {
const keys = JSON.parse(decodeURIComponent(keysMatch));
const { aes256, ed25519 } = keys;
aes256Key.current = aes256;
secondaryDeviceID.current = ed25519;
secondaryDeviceType.current = parsedData.data.deviceType;
} catch (err) {
console.log('Failed to decode URI component:', err);
return;
}
await processDeviceListUpdate();
},
[processDeviceListUpdate],
);
const handleBarCodeScanned = React.useCallback(
(barCodeEvent: BarCodeEvent) => {
setScanned(true);
Alert.alert(
'Connect with this device?',
'Are you sure you want to allow this device to log in to your account?',
[
{
text: 'Cancel',
style: 'cancel',
onPress: goBack,
},
{
text: 'Connect',
onPress: () => onConnect(barCodeEvent),
},
],
{ cancelable: false },
);
},
[goBack, onConnect],
);
if (hasPermission === null) {
return <View />;
}
if (deviceIsEmulator) {
return (
<View style={styles.textInputContainer}>
<Text style={styles.header}>QR Code URL</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={urlInput}
onChangeText={onChangeText}
placeholder="QR Code URL"
placeholderTextColor={panelForegroundTertiaryLabel}
autoFocus={true}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
</View>
);
}
// Note: According to the BarCodeScanner Expo docs, we should adhere to two
// guidances when using the BarCodeScanner:
// 1. We should specify the potential barCodeTypes we want to scan for to
// minimize battery usage.
// 2. We should set the onBarCodeScanned callback to undefined if it scanned
// in order to 'pause' the scanner from continuing to scan while we
// process the data from the scan.
// See: https://docs.expo.io/versions/latest/sdk/bar-code-scanner
return (
<View style={styles.scannerContainer}>
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
barCodeTypes={barCodeTypes}
style={styles.scanner}
/>
</View>
);
}
const unboundStyles = {
scannerContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
scanner: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
textInputContainer: {
paddingTop: 8,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
inputContainer: {
backgroundColor: 'panelForeground',
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingVertical: 12,
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
input: {
color: 'panelForegroundLabel',
flex: 1,
fontFamily: 'Arial',
fontSize: 16,
paddingVertical: 0,
borderBottomColor: 'transparent',
},
};
export default SecondaryDeviceQRCodeScanner;
diff --git a/native/root.react.js b/native/root.react.js
index d7d1ef30e..5cd3c46c9 100644
--- a/native/root.react.js
+++ b/native/root.react.js
@@ -1,447 +1,447 @@
// @flow
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type {
PossiblyStaleNavigationState,
UnsafeContainerActionEvent,
GenericNavigationAction,
} from '@react-navigation/core';
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
import { NavigationContainer } from '@react-navigation/native';
import * as SplashScreen from 'expo-splash-screen';
import invariant from 'invariant';
import * as React from 'react';
import { StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js';
import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js';
import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js';
import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js';
import { FarcasterChannelPrefetchHandler } from 'lib/components/farcaster-channel-prefetch-handler.react.js';
import { FarcasterDataHandler } from 'lib/components/farcaster-data-handler.react.js';
import { GlobalSearchIndexProvider } from 'lib/components/global-search-index-provider.react.js';
import IntegrityHandler from 'lib/components/integrity-handler.react.js';
import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js';
import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js';
import PlatformDetailsSynchronizer from 'lib/components/platform-details-synchronizer.react.js';
import PrekeysHandler from 'lib/components/prekeys-handler.react.js';
import { QRAuthProvider } from 'lib/components/qr-auth-provider.react.js';
import { StaffContextProvider } from 'lib/components/staff-provider.react.js';
import SyncCommunityStoreHandler from 'lib/components/sync-community-store-handler.react.js';
import { UserIdentityCacheProvider } from 'lib/components/user-identity-cache.react.js';
import { DBOpsHandler } from 'lib/handlers/db-ops-handler.react.js';
import { HoldersHandler } from 'lib/handlers/holders-handler.react.js';
import { InitialStateSharingHandler } from 'lib/handlers/initial-state-sharing-handler.react.js';
import { TunnelbrokerDeviceTokenHandler } from 'lib/handlers/tunnelbroker-device-token-handler.react.js';
import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js';
import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js';
import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js';
import KeyserverConnectionsHandler from 'lib/keyserver-conn/keyserver-connections-handler.js';
import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js';
import { actionLogger } from 'lib/utils/action-logger.js';
import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js';
import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js';
import BackupHandler from './backup/backup-handler.js';
import { BottomSheetProvider } from './bottom-sheet/bottom-sheet-provider.react.js';
import ChatContextProvider from './chat/chat-context-provider.react.js';
import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js';
import AccessTokenHandler from './components/access-token-handler.react.js';
import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js';
import BackgroundIdentityLoginHandler from './components/background-identity-login-handler.react.js';
import ConnectFarcasterAlertHandler from './components/connect-farcaster-alert-handler.react.js';
import DMActivityHandler from './components/dm-activity-handler.react.js';
import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js';
import { NUXTipsContextProvider } from './components/nux-tips-context.react.js';
import PersistedStateGate from './components/persisted-state-gate.js';
import ReportHandler from './components/report-handler.react.js';
import VersionSupportedChecker from './components/version-supported.react.js';
import ConnectedStatusBar from './connected-status-bar.react.js';
import { SQLiteDataHandler } from './data/sqlite-data-handler.js';
import ErrorBoundary from './error-boundary.react.js';
import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js';
import InputStateContainer from './input/input-state-container.react.js';
import LifecycleHandler from './lifecycle/lifecycle-handler.react.js';
import MarkdownContextProvider from './markdown/markdown-context-provider.react.js';
import { filesystemMediaCache } from './media/media-cache.js';
import { DeepLinksContextProvider } from './navigation/deep-links-context-provider.react.js';
import { defaultNavigationState } from './navigation/default-state.js';
import { setGlobalNavContext } from './navigation/icky-global.js';
import KeyserverReachabilityHandler from './navigation/keyserver-reachability-handler.js';
import {
NavContext,
type NavContextType,
} from './navigation/navigation-context.js';
import NavigationHandler from './navigation/navigation-handler.react.js';
import {
validNavState,
isShowingNUXTips,
} from './navigation/navigation-utils.js';
import OrientationHandler from './navigation/orientation-handler.react.js';
import { navStateAsyncStorageKey } from './navigation/persistance.js';
import RootNavigator from './navigation/root-navigator.react.js';
-import {
- composeTunnelbrokerQRAuthMessage,
- handleSecondaryDeviceLogInError,
- parseTunnelbrokerQRAuthMessage,
- performBackupRestore,
- generateQRAuthAESKey,
-} from './qr-code/qr-code-utils.js';
import ConnectivityUpdater from './redux/connectivity-updater.react.js';
import { DimensionsUpdater } from './redux/dimensions-updater.react.js';
import { getPersistor } from './redux/persist.js';
import { store } from './redux/redux-setup.js';
import { useSelector } from './redux/redux-utils.js';
import { RootContext } from './root-context.js';
import { MessageSearchProvider } from './search/search-provider.react.js';
import Socket from './socket.react.js';
import { useLoadCommFonts } from './themes/fonts.js';
import { DarkTheme, LightTheme } from './themes/navigation.js';
import ThemeHandler from './themes/theme-handler.react.js';
import { alchemyKey, ethersProvider } from './utils/ethers-utils.js';
import { neynarKey } from './utils/neynar-utils.js';
+import {
+ composeTunnelbrokerQRAuthMessage,
+ handleSecondaryDeviceLogInError,
+ parseTunnelbrokerQRAuthMessage,
+ performBackupRestore,
+ generateQRAuthAESKey,
+} from './utils/qr-code-utils.js';
// Add custom items to expo-dev-menu
import './dev-menu.js';
import './types/message-types-validator.js';
const navInitAction = Object.freeze({ type: 'NAV/@@INIT' });
const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' });
SplashScreen.preventAutoHideAsync().catch(console.log);
function Root() {
const navStateRef = React.useRef<?PossiblyStaleNavigationState>();
const navDispatchRef =
React.useRef<?(
action:
| GenericNavigationAction
| (PossiblyStaleNavigationState => GenericNavigationAction),
) => void>();
const navStateInitializedRef = React.useRef(false);
// We call this here to start the loading process
// We gate the UI on the fonts loading in AppNavigator
useLoadCommFonts();
const [navContext, setNavContext] = React.useState<?NavContextType>(null);
const updateNavContext = React.useCallback(() => {
if (
!navStateRef.current ||
!navDispatchRef.current ||
!navStateInitializedRef.current
) {
return;
}
const updatedNavContext = {
state: navStateRef.current,
dispatch: navDispatchRef.current,
};
setNavContext(updatedNavContext);
setGlobalNavContext(updatedNavContext);
}, []);
const [initialState, setInitialState] = React.useState(
__DEV__ ? undefined : defaultNavigationState,
);
React.useEffect(() => {
Orientation.lockToPortrait();
void (async () => {
let loadedState = initialState;
if (__DEV__) {
try {
const navStateString = await AsyncStorage.getItem(
navStateAsyncStorageKey,
);
if (navStateString) {
const savedState = JSON.parse(navStateString);
if (validNavState(savedState) && !isShowingNUXTips(savedState)) {
loadedState = savedState;
}
}
} catch {}
}
if (!loadedState) {
loadedState = defaultNavigationState;
}
if (loadedState !== initialState) {
setInitialState(loadedState);
}
navStateRef.current = loadedState;
updateNavContext();
actionLogger.addOtherAction('navState', navInitAction, null, loadedState);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateNavContext]);
const setNavStateInitialized = React.useCallback(() => {
navStateInitializedRef.current = true;
updateNavContext();
}, [updateNavContext]);
const [rootContext, setRootContext] = React.useState(() => ({
setNavStateInitialized,
}));
const detectUnsupervisedBackgroundRef = React.useCallback(
(detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => {
setRootContext(prevRootContext => ({
...prevRootContext,
detectUnsupervisedBackground,
}));
},
[],
);
const frozen = useSelector(state => state.frozen);
const queuedActionsRef = React.useRef<Array<GenericNavigationAction>>([]);
const onNavigationStateChange = React.useCallback(
(state: ?PossiblyStaleNavigationState) => {
invariant(state, 'nav state should be non-null');
const prevState = navStateRef.current;
navStateRef.current = state;
updateNavContext();
const queuedActions = queuedActionsRef.current;
queuedActionsRef.current = [];
if (queuedActions.length === 0) {
queuedActions.push(navUnknownAction);
}
for (const action of queuedActions) {
actionLogger.addOtherAction('navState', action, prevState, state);
}
if (!__DEV__ || frozen) {
return;
}
void (async () => {
try {
await AsyncStorage.setItem(
navStateAsyncStorageKey,
JSON.stringify(state),
);
} catch (e) {
console.log('AsyncStorage threw while trying to persist navState', e);
}
})();
},
[updateNavContext, frozen],
);
const navContainerRef =
React.useRef<?React.ElementRef<typeof NavigationContainer>>();
const containerRef = React.useCallback(
(navContainer: ?React.ElementRef<typeof NavigationContainer>) => {
navContainerRef.current = navContainer;
if (navContainer && !navDispatchRef.current) {
navDispatchRef.current = navContainer.dispatch;
updateNavContext();
}
},
[updateNavContext],
);
useReduxDevToolsExtension(navContainerRef);
const navContainer = navContainerRef.current;
React.useEffect(() => {
if (!navContainer) {
return undefined;
}
return navContainer.addListener(
'__unsafe_action__',
(event: { +data: UnsafeContainerActionEvent, ... }) => {
const { action, noop } = event.data;
const navState = navStateRef.current;
if (noop) {
actionLogger.addOtherAction('navState', action, navState, navState);
return;
}
queuedActionsRef.current.push({
...action,
type: `NAV/${action.type}`,
});
},
);
}, [navContainer]);
const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
const theme = (() => {
if (activeTheme === 'light') {
return LightTheme;
} else if (activeTheme === 'dark') {
return DarkTheme;
}
return undefined;
})();
const gated: React.Node = (
<>
<LifecycleHandler />
<KeyserverReachabilityHandler />
<DimensionsUpdater />
<ConnectivityUpdater />
<ThemeHandler />
<OrientationHandler />
<BackupHandler />
<IntegrityHandler />
<AccessTokenHandler />
<DBOpsHandler />
<UserInfosHandler />
<TunnelbrokerDeviceTokenHandler />
<HoldersHandler />
</>
);
let navigation;
if (initialState) {
navigation = (
<NavigationContainer
initialState={initialState}
onStateChange={onNavigationStateChange}
theme={theme}
ref={containerRef}
>
<BottomSheetModalProvider>
<ChatContextProvider>
<DeepLinksContextProvider>
<ChatMentionContextProvider>
<GlobalSearchIndexProvider>
<NUXTipsContextProvider>
<RootNavigator />
</NUXTipsContextProvider>
</GlobalSearchIndexProvider>
</ChatMentionContextProvider>
</DeepLinksContextProvider>
</ChatContextProvider>
<NavigationHandler />
<PersistedStateGate>
<FarcasterDataHandler>
<ConnectFarcasterAlertHandler />
</FarcasterDataHandler>
</PersistedStateGate>
</BottomSheetModalProvider>
</NavigationContainer>
);
}
return (
<GestureHandlerRootView style={styles.app}>
<StaffContextProvider>
<IdentityServiceContextProvider>
<UserIdentityCacheProvider>
<ENSCacheProvider
ethersProvider={ethersProvider}
alchemyKey={alchemyKey}
>
<NeynarClientProvider apiKey={neynarKey}>
<TunnelbrokerProvider>
<IdentitySearchProvider>
<QRAuthProvider
parseTunnelbrokerQRAuthMessage={
parseTunnelbrokerQRAuthMessage
}
composeTunnelbrokerQRAuthMessage={
composeTunnelbrokerQRAuthMessage
}
generateAESKey={generateQRAuthAESKey}
performBackupRestore={performBackupRestore}
onLogInError={handleSecondaryDeviceLogInError}
>
<FeatureFlagsProvider>
<NavContext.Provider value={navContext}>
<RootContext.Provider value={rootContext}>
<InputStateContainer>
<MessageEditingContextProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}
>
<ActionSheetProvider>
<MediaCacheProvider
persistence={filesystemMediaCache}
>
<EditUserAvatarProvider>
<NativeEditThreadAvatarProvider>
<MarkdownContextProvider>
<MessageSearchProvider>
<BottomSheetProvider>
<RegistrationContextProvider>
<SQLiteDataHandler />
<ConnectedStatusBar />
<ReduxPersistGate
persistor={getPersistor()}
>
{gated}
</ReduxPersistGate>
<PersistedStateGate>
<KeyserverConnectionsHandler
socketComponent={Socket}
detectUnsupervisedBackgroundRef={
detectUnsupervisedBackgroundRef
}
/>
<DMActivityHandler />
<VersionSupportedChecker />
<PlatformDetailsSynchronizer />
<BackgroundIdentityLoginHandler />
<PrekeysHandler />
<ReportHandler />
<FarcasterChannelPrefetchHandler />
<AutoJoinCommunityHandler />
<SyncCommunityStoreHandler />
<InitialStateSharingHandler />
</PersistedStateGate>
{navigation}
</RegistrationContextProvider>
</BottomSheetProvider>
</MessageSearchProvider>
</MarkdownContextProvider>
</NativeEditThreadAvatarProvider>
</EditUserAvatarProvider>
</MediaCacheProvider>
</ActionSheetProvider>
</SafeAreaProvider>
</MessageEditingContextProvider>
</InputStateContainer>
</RootContext.Provider>
</NavContext.Provider>
</FeatureFlagsProvider>
</QRAuthProvider>
</IdentitySearchProvider>
</TunnelbrokerProvider>
</NeynarClientProvider>
</ENSCacheProvider>
</UserIdentityCacheProvider>
</IdentityServiceContextProvider>
</StaffContextProvider>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
app: {
flex: 1,
},
});
function AppRoot(): React.Node {
return (
<Provider store={store}>
<CallKeyserverEndpointProvider>
<ErrorBoundary>
<Root />
</ErrorBoundary>
</CallKeyserverEndpointProvider>
</Provider>
);
}
export default AppRoot;
diff --git a/native/qr-code/qr-code-utils.js b/native/utils/qr-code-utils.js
similarity index 95%
rename from native/qr-code/qr-code-utils.js
rename to native/utils/qr-code-utils.js
index b7225a2a3..a93f6da63 100644
--- a/native/qr-code/qr-code-utils.js
+++ b/native/utils/qr-code-utils.js
@@ -1,102 +1,102 @@
// @flow
import { hexToUintArray } from 'lib/media/data-utils.js';
import type { BackupKeys } from 'lib/types/backup-types.js';
import {
peerToPeerMessageTypes,
type QRCodeAuthMessage,
} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js';
import {
qrCodeAuthMessagePayloadValidator,
type QRCodeAuthMessagePayload,
} from 'lib/types/tunnelbroker/qr-code-auth-message-types.js';
import { getMessageForException } from 'lib/utils/errors.js';
+import * as AES from './aes-crypto-module.js';
+import {
+ appOutOfDateAlertDetails,
+ unknownErrorAlertDetails,
+} from './alert-messages.js';
+import Alert from './alert.js';
import {
convertBytesToObj,
convertObjToBytes,
} from '../backup/conversion-utils.js';
import { commCoreModule, commUtilsModule } from '../native-modules.js';
import { persistConfig } from '../redux/persist.js';
-import * as AES from '../utils/aes-crypto-module.js';
-import {
- appOutOfDateAlertDetails,
- unknownErrorAlertDetails,
-} from '../utils/alert-messages.js';
-import Alert from '../utils/alert.js';
function composeTunnelbrokerQRAuthMessage(
encryptionKey: string,
obj: QRCodeAuthMessagePayload,
): Promise<QRCodeAuthMessage> {
const objBytes = convertObjToBytes(obj);
const keyBytes = hexToUintArray(encryptionKey);
const encryptedBytes = AES.encrypt(keyBytes, objBytes);
const encryptedContent = commUtilsModule.base64EncodeBuffer(
encryptedBytes.buffer,
);
return Promise.resolve({
type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE,
encryptedContent,
});
}
function parseTunnelbrokerQRAuthMessage(
encryptionKey: string,
message: QRCodeAuthMessage,
): Promise<?QRCodeAuthMessagePayload> {
const encryptedData = commUtilsModule.base64DecodeBuffer(
message.encryptedContent,
);
const decryptedData = AES.decrypt(
hexToUintArray(encryptionKey),
new Uint8Array(encryptedData),
);
const payload = convertBytesToObj<QRCodeAuthMessagePayload>(decryptedData);
if (!qrCodeAuthMessagePayloadValidator.is(payload)) {
return Promise.resolve(null);
}
return Promise.resolve(payload);
}
function handleSecondaryDeviceLogInError(error: mixed): void {
console.error('Secondary device log in error:', error);
const messageForException = getMessageForException(error);
if (
messageForException === 'client_version_unsupported' ||
messageForException === 'unsupported_version'
) {
Alert.alert(
appOutOfDateAlertDetails.title,
appOutOfDateAlertDetails.message,
);
} else {
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
);
}
}
function performBackupRestore(backupKeys: BackupKeys): Promise<void> {
const { backupID, backupDataKey, backupLogDataKey } = backupKeys;
return commCoreModule.restoreBackupData(
backupID,
backupDataKey,
backupLogDataKey,
persistConfig.version.toString(),
);
}
function generateQRAuthAESKey(): Promise<Uint8Array> {
return Promise.resolve(AES.generateKey());
}
export {
composeTunnelbrokerQRAuthMessage,
parseTunnelbrokerQRAuthMessage,
handleSecondaryDeviceLogInError,
performBackupRestore,
generateQRAuthAESKey,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:25 AM (3 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690157
Default Alt Text
(48 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment