Page MenuHomePhabricator

No OneTemporary

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

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:25 AM (8 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690157
Default Alt Text
(48 KB)

Event Timeline