diff --git a/native/account/qr-auth/qr-auth-context-provider.js b/native/account/qr-auth/qr-auth-context-provider.js
index b1727fbe4..ea115e643 100644
--- a/native/account/qr-auth/qr-auth-context-provider.js
+++ b/native/account/qr-auth/qr-auth-context-provider.js
@@ -1,259 +1,275 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
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 {
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 { QRAuthContext } from './qr-auth-context.js';
import { commCoreModule } from '../../native-modules.js';
import { useSelector } from '../../redux/redux-utils.js';
import Alert from '../../utils/alert.js';
import {
composeTunnelbrokerQRAuthMessage,
parseTunnelbrokerQRAuthMessage,
} from '../../utils/qr-code-utils.js';
type Props = {
+children: React.Node,
};
function QRAuthContextProvider(props: Props): React.Node {
const aes256Key = React.useRef(null);
const secondaryDeviceID = React.useRef(null);
const secondaryDeviceType = React.useRef(null);
const [connectingInProgress, setConnectingInProgress] = React.useState(false);
const ownPeerDevices = useSelector(getOwnPeerDevices);
const keyserverDeviceID = getKeyserverDeviceID(ownPeerDevices);
const { goBack } = useNavigation();
const runDeviceListUpdate = useDeviceListUpdate();
const { addListener, removeListener, sendMessageToDevice } =
useTunnelbroker();
const identityContext = React.useContext(IdentityClientContext);
invariant(identityContext, 'identity context not set');
const tunnelbrokerMessageListener = React.useCallback(
async (message: TunnelbrokerToDeviceMessage) => {
+ if (!connectingInProgress) {
+ return;
+ }
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;
}
setConnectingInProgress(false);
Alert.alert('Device added', 'Device registered successfully', [
{ text: 'OK', onPress: goBack },
]);
},
- [goBack],
+ [goBack, connectingInProgress],
);
React.useEffect(() => {
addListener(tunnelbrokerMessageListener);
return () => {
removeListener(tunnelbrokerMessageListener);
};
}, [addListener, removeListener, tunnelbrokerMessageListener]);
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) {
backupData = await commCoreModule.getQRAuthBackupData();
}
const message = await composeTunnelbrokerQRAuthMessage(encryptionKey, {
type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS,
userID,
primaryDeviceID,
backupData,
});
await 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) {
setConnectingInProgress(false);
console.log('Primary device error:', err);
Alert.alert('Adding device failed', 'Failed to update the device list', [
{ text: 'OK' },
]);
goBack();
}
}, [
goBack,
identityContext,
keyserverDeviceID,
runDeviceListUpdate,
sendMessageToDevice,
]);
const onConnect = React.useCallback(
async (data: string) => {
setConnectingInProgress(true);
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' }],
);
setConnectingInProgress(false);
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) {
setConnectingInProgress(false);
console.log('Failed to decode URI component:', err);
return;
}
await processDeviceListUpdate();
},
[processDeviceListUpdate],
);
+ const onRemoveSecondaryDevice = React.useCallback(async () => {
+ if (!secondaryDeviceID.current) {
+ console.log('No secondary device to remove');
+ return;
+ }
+
+ await runDeviceListUpdate({
+ type: 'remove',
+ deviceID: secondaryDeviceID.current,
+ });
+ }, [runDeviceListUpdate]);
+
const contextValue = React.useMemo(
() => ({
onConnect,
connectingInProgress,
+ onRemoveSecondaryDevice,
}),
- [onConnect, connectingInProgress],
+ [onConnect, connectingInProgress, onRemoveSecondaryDevice],
);
return (
{props.children}
);
}
export { QRAuthContextProvider };
diff --git a/native/account/qr-auth/qr-auth-context.js b/native/account/qr-auth/qr-auth-context.js
index a9caba016..b94174301 100644
--- a/native/account/qr-auth/qr-auth-context.js
+++ b/native/account/qr-auth/qr-auth-context.js
@@ -1,13 +1,14 @@
// @flow
import * as React from 'react';
export type QRAuthContextType = {
+onConnect: (data: string) => Promise,
+connectingInProgress: boolean,
+ +onRemoveSecondaryDevice: () => Promise,
};
const QRAuthContext: React.Context =
React.createContext();
export { QRAuthContext };