diff --git a/native/profile/linked-devices-header-right-button.react.js b/native/profile/linked-devices-header-right-button.react.js
--- a/native/profile/linked-devices-header-right-button.react.js
+++ b/native/profile/linked-devices-header-right-button.react.js
@@ -5,22 +5,11 @@
import HeaderRightTextButton from '../navigation/header-right-text-button.react.js';
import { SecondaryDeviceQRCodeScannerRouteName } from '../navigation/route-names.js';
-import Alert from '../utils/alert.js';
-import { deviceIsEmulator } from '../utils/url-utils.js';
function LinkedDevicesHeaderRightButton(): React.Node {
const { navigate } = useNavigation();
const navigateToQRCodeScanner = React.useCallback(() => {
- if (deviceIsEmulator) {
- Alert.alert(
- 'Unsupported device',
- "You can't access the QR code scanner on a simulator.",
- [{ text: 'OK' }],
- { cancelable: false },
- );
- return;
- }
navigate(SecondaryDeviceQRCodeScannerRouteName);
}, [navigate]);
diff --git a/native/profile/profile.react.js b/native/profile/profile.react.js
--- a/native/profile/profile.react.js
+++ b/native/profile/profile.react.js
@@ -57,6 +57,7 @@
} from '../navigation/route-names.js';
import type { TabNavigationProp } from '../navigation/tab-navigator.react.js';
import { useStyles, useColors } from '../themes/colors.js';
+import { deviceIsEmulator } from '../utils/url-utils.js';
const header = (props: StackHeaderProps) => ;
const profileScreenOptions = { headerTitle: 'Profile' };
@@ -78,7 +79,7 @@
const backupMenuOptions = { headerTitle: 'Backup menu' };
const tunnelbrokerMenuOptions = { headerTitle: 'Tunnelbroker menu' };
const secondaryDeviceQRCodeScannerOptions = {
- headerTitle: '',
+ headerTitle: deviceIsEmulator ? 'Link device' : '',
headerBackTitleVisible: false,
};
const buildInfoOptions = { headerTitle: 'Build info' };
diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js
--- a/native/profile/secondary-device-qr-code-scanner.react.js
+++ b/native/profile/secondary-device-qr-code-scanner.react.js
@@ -4,7 +4,7 @@
import { BarCodeScanner, type BarCodeEvent } from 'expo-barcode-scanner';
import invariant from 'invariant';
import * as React from 'react';
-import { View } from 'react-native';
+import { View, Text } from 'react-native';
import { parseDataFromDeepLink } from 'lib/facts/links.js';
import {
@@ -34,15 +34,18 @@
import type { ProfileNavigationProp } from './profile.react.js';
import { getBackupSecret } from '../backup/use-client-backup.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 } from '../themes/colors.js';
+import { useStyles, useColors } from '../themes/colors.js';
import Alert from '../utils/alert.js';
+import { deviceIsEmulator } from '../utils/url-utils.js';
const barCodeTypes = [BarCodeScanner.Constants.BarCodeType.qr];
@@ -54,9 +57,10 @@
function SecondaryDeviceQRCodeScanner(props: Props): React.Node {
const [hasPermission, setHasPermission] = React.useState(null);
const [scanned, setScanned] = React.useState(false);
+ const [urlInput, setURLInput] = React.useState('');
const styles = useStyles(unboundStyles);
- const navigation = useNavigation();
+ const { goBack, setOptions } = useNavigation();
const tunnelbrokerContext = useTunnelbroker();
const identityContext = React.useContext(IdentityClientContext);
@@ -70,6 +74,8 @@
const foreignPeerDevices = useSelector(getForeignPeerDevices);
+ const { panelForegroundTertiaryLabel } = useColors();
+
const tunnelbrokerMessageListener = React.useCallback(
async (message: TunnelbrokerMessage) => {
const encryptionKey = aes256Key.current;
@@ -132,7 +138,7 @@
if (!payload.requestBackupKeys) {
Alert.alert('Device added', 'Device registered successfully', [
- { text: 'OK' },
+ { text: 'OK', onPress: goBack },
]);
return;
}
@@ -158,7 +164,7 @@
});
Alert.alert('Device added', 'Device registered successfully', [
- { text: 'OK' },
+ { text: 'OK', onPress: goBack },
]);
},
[
@@ -167,6 +173,7 @@
foreignPeerDevices,
getAndUpdateDeviceListsForUsers,
tunnelbrokerContext,
+ goBack,
],
);
@@ -190,10 +197,94 @@
[{ text: 'OK' }],
);
- navigation.goBack();
+ goBack();
}
})();
- }, [navigation]);
+ }, [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');
+ }
+ await addDeviceToDeviceList(
+ identityContext.identityClient,
+ userID,
+ targetDeviceID,
+ );
+ const message = await composeTunnelbrokerQRAuthMessage(encryptionKey, {
+ type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS,
+ userID,
+ primaryDeviceID,
+ });
+ await tunnelbrokerContext.sendMessage({
+ deviceID: targetDeviceID,
+ payload: JSON.stringify(message),
+ });
+ } catch (err) {
+ console.log('Primary device error:', err);
+ Alert.alert('Adding device failed', 'Failed to update the device list', [
+ { text: 'OK' },
+ ]);
+ goBack();
+ }
+ }, [goBack, identityContext, 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;
+ } catch (err) {
+ console.log('Failed to decode URI component:', err);
+ }
+ await processDeviceListUpdate();
+ }, [processDeviceListUpdate, urlInput]);
+
+ const buttonDisabled = !urlInput;
+ React.useEffect(() => {
+ if (!deviceIsEmulator) {
+ return;
+ }
+ setOptions({
+ headerRight: () => (
+
+ ),
+ });
+ }, [buttonDisabled, onPressSave, setOptions]);
+
+ const onChangeText = React.useCallback(
+ (text: string) => setURLInput(text),
+ [],
+ );
const onConnect = React.useCallback(
async (barCodeEvent: BarCodeEvent) => {
@@ -210,42 +301,18 @@
return;
}
- const keys = JSON.parse(decodeURIComponent(keysMatch));
- const { aes256, ed25519 } = keys;
- aes256Key.current = aes256;
- secondaryDeviceID.current = ed25519;
-
try {
- const { deviceID: primaryDeviceID, userID } =
- await identityContext.getAuthMetadata();
- if (!primaryDeviceID || !userID) {
- throw new Error('missing auth metadata');
- }
- await addDeviceToDeviceList(
- identityContext.identityClient,
- userID,
- ed25519,
- );
- const message = await composeTunnelbrokerQRAuthMessage(aes256, {
- type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS,
- userID,
- primaryDeviceID,
- });
- await tunnelbrokerContext.sendMessage({
- deviceID: ed25519,
- payload: JSON.stringify(message),
- });
+ const keys = JSON.parse(decodeURIComponent(keysMatch));
+ const { aes256, ed25519 } = keys;
+ aes256Key.current = aes256;
+ secondaryDeviceID.current = ed25519;
} catch (err) {
- console.log('Primary device error:', err);
- Alert.alert(
- 'Adding device failed',
- 'Failed to update the device list',
- [{ text: 'OK' }],
- );
- navigation.goBack();
+ console.log('Failed to decode URI component:', err);
}
+
+ await processDeviceListUpdate();
},
- [tunnelbrokerContext, identityContext, navigation],
+ [processDeviceListUpdate],
);
const onCancelScan = React.useCallback(() => setScanned(false), []);
@@ -277,6 +344,25 @@
return ;
}
+ if (deviceIsEmulator) {
+ return (
+
+ QR Code URL
+
+
+
+
+ );
+ }
// 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
@@ -286,7 +372,7 @@
// process the data from the scan.
// See: https://docs.expo.io/versions/latest/sdk/bar-code-scanner
return (
-
+