diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js
index 0fae56caa..8cd0c504b 100644
--- a/native/profile/secondary-device-qr-code-scanner.react.js
+++ b/native/profile/secondary-device-qr-code-scanner.react.js
@@ -1,132 +1,206 @@
// @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 } from 'react-native';
import { parseDataFromDeepLink } from 'lib/facts/links.js';
+import { IdentityClientContext } from 'lib/shared/identity-client-context.js';
+import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js';
+import type { RawDeviceList } from 'lib/types/identity-service-types.js';
+import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js';
+import { createQRAuthTunnelbrokerMessage } from 'lib/utils/qr-code-auth.js';
import type { ProfileNavigationProp } from './profile.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
import Alert from '../utils/alert.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(null);
const [scanned, setScanned] = React.useState(false);
const styles = useStyles(unboundStyles);
const navigation = useNavigation();
+ const tunnelbrokerContext = useTunnelbroker();
+ const identityContext = React.useContext(IdentityClientContext);
+ invariant(identityContext, 'identity context not set');
+
+ const addDeviceToList = React.useCallback(
+ async (newDeviceID: string) => {
+ const { getDeviceListHistoryForUser, updateDeviceList } =
+ identityContext.identityClient;
+ invariant(
+ updateDeviceList,
+ 'updateDeviceList() should be defined for primary device',
+ );
+
+ const authMetadata = await identityContext.getAuthMetadata();
+ if (!authMetadata?.userID) {
+ throw new Error('missing auth metadata');
+ }
+
+ const deviceLists = await getDeviceListHistoryForUser(
+ authMetadata.userID,
+ );
+ invariant(deviceLists.length > 0, 'received empty device list history');
+
+ const lastSignedDeviceList = deviceLists[deviceLists.length - 1];
+ const deviceList: RawDeviceList = JSON.parse(
+ lastSignedDeviceList.rawDeviceList,
+ );
+
+ const { devices } = deviceList;
+ if (devices.includes(newDeviceID)) {
+ return;
+ }
+
+ const newDeviceList: RawDeviceList = {
+ devices: [...devices, newDeviceID],
+ timestamp: Date.now(),
+ };
+ await updateDeviceList({
+ rawDeviceList: JSON.stringify(newDeviceList),
+ });
+ },
+ [identityContext],
+ );
+
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' }],
);
navigation.goBack();
}
})();
}, [navigation]);
- const onConnect = React.useCallback((barCodeEvent: BarCodeEvent) => {
- const { data } = barCodeEvent;
- const parsedData = parseDataFromDeepLink(data);
- const keysMatch = parsedData?.data?.keys;
+ 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;
- }
-
- const keys = JSON.parse(decodeURIComponent(keysMatch));
+ if (!parsedData || !keysMatch) {
+ Alert.alert(
+ 'Scan failed',
+ 'QR code does not contain a valid pair of keys.',
+ [{ text: 'OK' }],
+ );
+ return;
+ }
- Alert.alert(
- 'Scan successful',
- `QR code contains the following keys: ${JSON.stringify(keys)}`,
- [{ text: 'OK' }],
- );
- }, []);
+ const keys = JSON.parse(decodeURIComponent(keysMatch));
+ const { aes256, ed25519 } = keys;
+
+ try {
+ const { deviceID: primaryDeviceID, userID } =
+ await identityContext.getAuthMetadata();
+ if (!primaryDeviceID || !userID) {
+ throw new Error('missing auth metadata');
+ }
+ await addDeviceToList(ed25519);
+ const message = createQRAuthTunnelbrokerMessage(aes256, {
+ type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS,
+ userID,
+ primaryDeviceID,
+ });
+ await tunnelbrokerContext.sendMessage({
+ deviceID: ed25519,
+ 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' }],
+ );
+ navigation.goBack();
+ }
+ },
+ [tunnelbrokerContext, addDeviceToList, identityContext, navigation],
+ );
const onCancelScan = React.useCallback(() => setScanned(false), []);
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: onCancelScan,
},
{
text: 'Connect',
onPress: () => onConnect(barCodeEvent),
},
],
{ cancelable: false },
);
},
[onCancelScan, onConnect],
);
if (hasPermission === null) {
return ;
}
// 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 (
);
}
const unboundStyles = {
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
scanner: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
};
export default SecondaryDeviceQRCodeScanner;