diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js index 5b8161aff..f5021a43b 100644 --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -1,142 +1,154 @@ // @flow import { QRCodeSVG } from 'qrcode.react'; import * as React from 'react'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; import { useSecondaryDeviceLogIn } from 'lib/hooks/login-hooks.js'; import { useQRAuth } from 'lib/hooks/qr-auth.js'; import { generateKeyCommon } from 'lib/media/aes-crypto-utils-common.js'; import * as AES from 'lib/media/aes-crypto-utils-common.js'; import { hexToUintArray, uintArrayToHexString } from 'lib/media/data-utils.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.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 { getContentSigningKey } from 'lib/utils/crypto-utils.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import css from './qr-code-login.css'; +import VersionUnsupportedModal from '../modals/version-unsupported-modal.react.js'; import { base64DecodeBuffer, base64EncodeBuffer, } from '../utils/base64-utils.js'; import { convertBytesToObj, convertObjToBytes, } from '../utils/conversion-utils.js'; async function composeTunnelbrokerMessage( encryptionKey: string, obj: QRCodeAuthMessagePayload, ): Promise { const objBytes = convertObjToBytes(obj); const keyBytes = hexToUintArray(encryptionKey); const encryptedBytes = await AES.encryptCommon(crypto, keyBytes, objBytes); const encryptedContent = base64EncodeBuffer(encryptedBytes); return { type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, encryptedContent, }; } async function parseTunnelbrokerMessage( encryptionKey: string, message: QRCodeAuthMessage, ): Promise { const encryptedData = base64DecodeBuffer(message.encryptedContent); const decryptedData = await AES.decryptCommon( crypto, hexToUintArray(encryptionKey), new Uint8Array(encryptedData), ); const payload = convertBytesToObj(decryptedData); if (!qrCodeAuthMessagePayloadValidator.is(payload)) { return null; } return payload; } function QRCodeLogin(): React.Node { const [qrData, setQRData] = React.useState(); const { setUnauthorizedDeviceID } = useTunnelbroker(); const generateQRCode = React.useCallback(async () => { try { const [ed25519, rawAESKey] = await Promise.all([ getContentSigningKey(), generateKeyCommon(crypto), ]); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); setUnauthorizedDeviceID(ed25519); setQRData({ deviceID: ed25519, aesKey: aesKeyAsHexString }); } catch (err) { console.error('Failed to generate QR Code:', err); } }, [setUnauthorizedDeviceID]); + const { pushModal } = useModalContext(); + const logInSecondaryDevice = useSecondaryDeviceLogIn(); const performRegistration = React.useCallback( async (userID: string) => { try { await logInSecondaryDevice(userID); } catch (err) { console.error('Secondary device registration error:', err); + const messageForException = getMessageForException(err); + if ( + messageForException === 'client_version_unsupported' || + messageForException === 'Unsupported version' + ) { + pushModal(); + } void generateQRCode(); } }, - [logInSecondaryDevice, generateQRCode], + [logInSecondaryDevice, pushModal, generateQRCode], ); React.useEffect(() => { void generateQRCode(); }, [generateQRCode]); const qrCodeURL = React.useMemo( () => (qrData ? qrCodeLinkURL(qrData.aesKey, qrData.deviceID) : undefined), [qrData], ); const qrAuthInput = React.useMemo( () => ({ secondaryDeviceID: qrData?.deviceID, aesKey: qrData?.aesKey, performSecondaryDeviceRegistration: performRegistration, composeMessage: composeTunnelbrokerMessage, processMessage: parseTunnelbrokerMessage, }), [qrData, performRegistration], ); useQRAuth(qrAuthInput); return (
Log in to Comm
Open the Comm app on your phone and scan the QR code below
How to find the scanner:
Go to Profile
Select Linked devices
Click Add on the top right
); } export default QRCodeLogin; diff --git a/web/components/log-out-if-missing-csat-handler.react.js b/web/components/log-out-if-missing-csat-handler.react.js index 5841aa343..04e4cd4f8 100644 --- a/web/components/log-out-if-missing-csat-handler.react.js +++ b/web/components/log-out-if-missing-csat-handler.react.js @@ -1,84 +1,84 @@ // @flow import * as React from 'react'; import { logOutActionTypes, useLogOut } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { securityUpdateLogoutText } from 'lib/types/alert-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useSelector } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; -import css from './version-handler.css'; +import css from './missing-csat-modal.css'; import Modal from '../modals/modal.react.js'; type Props = { +isAccountWithPassword: boolean, }; function MissingCSATModal(props: Props): React.Node { const { popModal } = useModalContext(); const { isAccountWithPassword } = props; let modalContent; if (isAccountWithPassword) { modalContent = (

Unfortunately, we must log you out in order to generate a PAKE-derived secret. You can learn more about PAKEs and the protocol we use (OPAQUE){' '} here .

); } else { modalContent =

{securityUpdateLogoutText}

; } return (
{modalContent}
); } function LogOutIfMissingCSATHandler() { const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const hasAccessToken = useSelector(state => !!state.commServicesAccessToken); const dataLoaded = useSelector(state => state.dataLoaded); const { pushModal } = useModalContext(); React.useEffect(() => { if (!hasAccessToken && dataLoaded && usingCommServicesAccessToken) { void dispatchActionPromise(logOutActionTypes, callLogOut()); pushModal( , ); } }, [ callLogOut, dataLoaded, dispatchActionPromise, hasAccessToken, isAccountWithPassword, pushModal, ]); } export default LogOutIfMissingCSATHandler; diff --git a/web/components/missing-csat-modal.css b/web/components/missing-csat-modal.css new file mode 100644 index 000000000..3247d4291 --- /dev/null +++ b/web/components/missing-csat-modal.css @@ -0,0 +1,6 @@ +.modalContent { + color: var(--text-background-primary-default); + font-size: var(--s-font-14); + margin-top: 8px; + display: flex; +} diff --git a/web/components/version-handler.css b/web/components/version-handler.css deleted file mode 100644 index 68d503365..000000000 --- a/web/components/version-handler.css +++ /dev/null @@ -1,21 +0,0 @@ -.modalContent { - color: var(--text-background-primary-default); - font-size: var(--s-font-14); - margin-top: 8px; - display: flex; -} - -.wrapper { - display: flex; - flex-shrink: 1; - justify-content: center; -} - -.ancestryContainer { - display: flex; - justify-content: center; - align-items: center; - height: 40px; - background-color: var(--frame-background-tertiary-default); - margin-top: 20px; -} diff --git a/web/components/version-handler.react.js b/web/components/version-handler.react.js index 6cdce5ecc..64bd8ac81 100644 --- a/web/components/version-handler.react.js +++ b/web/components/version-handler.react.js @@ -1,46 +1,34 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { allConnectionInfosSelector } from 'lib/selectors/keyserver-selectors.js'; -import css from './version-handler.css'; -import Modal from '../modals/modal.react.js'; +import VersionUnsupportedModal from '../modals/version-unsupported-modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getVersionUnsupportedError } from '../utils/version-utils.js'; - -function VersionUnsupportedModal(): React.Node { - const { popModal } = useModalContext(); - const message = getVersionUnsupportedError(); - return ( - -
{message}
-
- ); -} function MinVersionHandler(): null { const connections = useSelector(allConnectionInfosSelector); const isClientVersionUnsupported = React.useMemo(() => { const connectionIssues = Object.values(connections).map( connection => connection?.connectionIssue, ); return connectionIssues.includes('client_version_unsupported'); }, [connections]); const hasShownModalRef = React.useRef(false); const { pushModal } = useModalContext(); React.useEffect(() => { if (isClientVersionUnsupported && !hasShownModalRef.current) { hasShownModalRef.current = true; pushModal(); } }, [isClientVersionUnsupported, pushModal]); return null; } export default MinVersionHandler; diff --git a/web/modals/version-unsupported-modal.css b/web/modals/version-unsupported-modal.css new file mode 100644 index 000000000..3247d4291 --- /dev/null +++ b/web/modals/version-unsupported-modal.css @@ -0,0 +1,6 @@ +.modalContent { + color: var(--text-background-primary-default); + font-size: var(--s-font-14); + margin-top: 8px; + display: flex; +} diff --git a/web/modals/version-unsupported-modal.react.js b/web/modals/version-unsupported-modal.react.js new file mode 100644 index 000000000..666fc1889 --- /dev/null +++ b/web/modals/version-unsupported-modal.react.js @@ -0,0 +1,21 @@ +// @flow + +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; + +import css from './version-unsupported-modal.css'; +import Modal from '../modals/modal.react.js'; +import { getVersionUnsupportedError } from '../utils/version-utils.js'; + +function VersionUnsupportedModal(): React.Node { + const { popModal } = useModalContext(); + const message = getVersionUnsupportedError(); + return ( + +
{message}
+
+ ); +} + +export default VersionUnsupportedModal;