diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js
--- a/web/account/qr-code-login.react.js
+++ b/web/account/qr-code-login.react.js
@@ -3,55 +3,13 @@
 import { QRCodeSVG } from 'qrcode.react';
 import * as React from 'react';
 
+import { useQRAuthContext } from 'lib/components/qr-auth-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 { uintArrayToHexString } from 'lib/media/data-utils.js';
-import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js';
-import { getContentSigningKey } from 'lib/utils/crypto-utils.js';
 
 import css from './qr-code-login.css';
-import {
-  composeTunnelbrokerQRAuthMessage,
-  parseTunnelbrokerQRAuthMessage,
-  useHandleSecondaryDeviceRegistrationError,
-} from '../utils/qr-code-utils.js';
 
 function QRCodeLogin(): React.Node {
-  const [qrData, setQRData] =
-    React.useState<?{ +deviceID: string, +aesKey: string }>();
-
-  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 handleError = useHandleSecondaryDeviceRegistrationError();
-
-  const logInSecondaryDevice = useSecondaryDeviceLogIn();
-  const performRegistration = React.useCallback(
-    async (userID: string) => {
-      try {
-        await logInSecondaryDevice(userID);
-      } catch (err) {
-        handleError(err);
-        void generateQRCode();
-      }
-    },
-    [logInSecondaryDevice, handleError, generateQRCode],
-  );
+  const { qrData, generateQRCode } = useQRAuthContext();
 
   React.useEffect(() => {
     void generateQRCode();
@@ -62,18 +20,6 @@
     [qrData],
   );
 
-  const qrAuthInput = React.useMemo(
-    () => ({
-      secondaryDeviceID: qrData?.deviceID,
-      aesKey: qrData?.aesKey,
-      performSecondaryDeviceRegistration: performRegistration,
-      composeMessage: composeTunnelbrokerQRAuthMessage,
-      processMessage: parseTunnelbrokerQRAuthMessage,
-    }),
-    [qrData, performRegistration],
-  );
-  useQRAuth(qrAuthInput);
-
   return (
     <div className={css.qrContainer}>
       <div className={css.title}>Log in to Comm</div>
diff --git a/web/app.react.js b/web/app.react.js
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -21,6 +21,7 @@
 } from 'lib/components/modal-provider.react.js';
 import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js';
 import PlatformDetailsSynchronizer from 'lib/components/platform-details-synchronizer.react.js';
+import { QRAuthProvider } from 'lib/components/qr-auth-provider.react.js';
 import { StaffContextProvider } from 'lib/components/staff-provider.react.js';
 import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js';
 import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js';
@@ -87,6 +88,12 @@
 import css from './style.css';
 import { TooltipProvider } from './tooltips/tooltip-provider.js';
 import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js';
+import {
+  composeTunnelbrokerQRAuthMessage,
+  generateQRAuthKey,
+  parseTunnelbrokerQRAuthMessage,
+  useHandleSecondaryDeviceRegistrationError,
+} from './utils/qr-code-utils.js';
 import { useWebLock, TUNNELBROKER_LOCK_NAME } from './web-lock.js';
 
 // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS,
@@ -536,6 +543,9 @@
     const secondaryTunnelbrokerConnection: SecondaryTunnelbrokerConnection =
       useOtherTabsTunnelbrokerConnection();
 
+    const handleSecondaryDeviceRegistrationError =
+      useHandleSecondaryDeviceRegistrationError();
+
     return (
       <AppThemeWrapper>
         <TunnelbrokerProvider
@@ -544,15 +554,22 @@
           secondaryTunnelbrokerConnection={secondaryTunnelbrokerConnection}
         >
           <IdentitySearchProvider>
-            <App
-              {...props}
-              navInfo={navInfo}
-              entriesLoadingStatus={entriesLoadingStatus}
-              loggedIn={loggedIn}
-              activeThreadCurrentlyUnread={activeThreadCurrentlyUnread}
-              dispatch={dispatch}
-              modals={modals}
-            />
+            <QRAuthProvider
+              processTunnelbrokerMessage={parseTunnelbrokerQRAuthMessage}
+              composeTunnelbrokerMessage={composeTunnelbrokerQRAuthMessage}
+              generateAESKey={generateQRAuthKey}
+              onLoginError={handleSecondaryDeviceRegistrationError}
+            >
+              <App
+                {...props}
+                navInfo={navInfo}
+                entriesLoadingStatus={entriesLoadingStatus}
+                loggedIn={loggedIn}
+                activeThreadCurrentlyUnread={activeThreadCurrentlyUnread}
+                dispatch={dispatch}
+                modals={modals}
+              />
+            </QRAuthProvider>
             <DBOpsHandler />
           </IdentitySearchProvider>
         </TunnelbrokerProvider>
diff --git a/web/utils/qr-code-utils.js b/web/utils/qr-code-utils.js
--- a/web/utils/qr-code-utils.js
+++ b/web/utils/qr-code-utils.js
@@ -4,6 +4,7 @@
 
 import { useModalContext } from 'lib/components/modal-provider.react.js';
 import * as AES from 'lib/media/aes-crypto-utils-common.js';
+import { generateKeyCommon } from 'lib/media/aes-crypto-utils-common.js';
 import { hexToUintArray } from 'lib/media/data-utils.js';
 import {
   peerToPeerMessageTypes,
@@ -74,8 +75,13 @@
   );
 }
 
+function generateQRAuthKey(): Promise<Uint8Array> {
+  return generateKeyCommon(crypto);
+}
+
 export {
   composeTunnelbrokerQRAuthMessage,
   parseTunnelbrokerQRAuthMessage,
   useHandleSecondaryDeviceRegistrationError,
+  generateQRAuthKey,
 };