Parcourir la source

[web] Enable Typescript's strict mode for auth's code (#1865)

Manav Rathi il y a 1 an
Parent
commit
582eb9e1ea
68 fichiers modifiés avec 528 ajouts et 446 suppressions
  1. 7 10
      web/apps/accounts/src/pages/_app.tsx
  2. 13 15
      web/apps/auth/src/pages/_app.tsx
  3. 33 32
      web/apps/auth/src/pages/auth.tsx
  4. 7 4
      web/apps/auth/src/pages/change-email.tsx
  5. 7 4
      web/apps/auth/src/pages/change-password.tsx
  6. 7 4
      web/apps/auth/src/pages/credentials.tsx
  7. 7 4
      web/apps/auth/src/pages/generate.tsx
  8. 3 3
      web/apps/auth/src/pages/index.tsx
  9. 7 4
      web/apps/auth/src/pages/login.tsx
  10. 2 10
      web/apps/auth/src/pages/passkeys/finish.tsx
  11. 7 4
      web/apps/auth/src/pages/recover.tsx
  12. 7 4
      web/apps/auth/src/pages/signup.tsx
  13. 7 4
      web/apps/auth/src/pages/two-factor/recover.tsx
  14. 7 4
      web/apps/auth/src/pages/two-factor/setup.tsx
  15. 7 4
      web/apps/auth/src/pages/two-factor/verify.tsx
  16. 7 4
      web/apps/auth/src/pages/verify.tsx
  17. 3 3
      web/apps/auth/src/services/code.ts
  18. 8 7
      web/apps/auth/src/services/remote.ts
  19. 6 11
      web/apps/auth/tsconfig.json
  20. 5 4
      web/apps/photos/src/components/Sidebar/index.tsx
  21. 1 1
      web/apps/photos/src/components/Upload/CollectionMappingChoiceModal.tsx
  22. 9 6
      web/apps/photos/src/components/WatchFolder.tsx
  23. 7 10
      web/apps/photos/src/pages/_app.tsx
  24. 82 76
      web/apps/photos/src/pages/index.tsx
  25. 1 1
      web/packages/accounts/api/srp.ts
  26. 16 15
      web/packages/accounts/api/user.ts
  27. 28 15
      web/packages/accounts/components/ChangeEmail.tsx
  28. 2 1
      web/packages/accounts/components/SetPasswordForm.tsx
  29. 2 1
      web/packages/accounts/components/SignUp.tsx
  30. 3 2
      web/packages/accounts/components/two-factor/VerifyForm.tsx
  31. 2 2
      web/packages/accounts/components/two-factor/setup/ManualMode.tsx
  32. 1 1
      web/packages/accounts/components/two-factor/setup/QRMode.tsx
  33. 1 1
      web/packages/accounts/components/two-factor/setup/index.tsx
  34. 7 4
      web/packages/accounts/pages/change-password.tsx
  35. 16 8
      web/packages/accounts/pages/credentials.tsx
  36. 27 18
      web/packages/accounts/pages/generate.tsx
  37. 10 5
      web/packages/accounts/pages/recover.tsx
  38. 7 6
      web/packages/accounts/pages/two-factor/recover.tsx
  39. 7 4
      web/packages/accounts/pages/two-factor/setup.tsx
  40. 2 1
      web/packages/accounts/pages/two-factor/verify.tsx
  41. 2 1
      web/packages/accounts/pages/verify.tsx
  42. 8 2
      web/packages/build-config/tsconfig-next.json
  43. 3 1
      web/packages/build-config/tsconfig-typecheck.json
  44. 2 2
      web/packages/shared/apps/types.ts
  45. 1 1
      web/packages/shared/components/CodeBlock/index.tsx
  46. 8 3
      web/packages/shared/components/DialogBox/TitleWithCloseButton.tsx
  47. 1 3
      web/packages/shared/components/DialogBox/index.tsx
  48. 5 3
      web/packages/shared/components/DialogBoxV2/index.tsx
  49. 0 4
      web/packages/shared/components/DialogBoxV2/types.ts
  50. 6 11
      web/packages/shared/components/EnteButton.tsx
  51. 16 6
      web/packages/shared/components/EnteLogo.tsx
  52. 7 2
      web/packages/shared/components/Navbar/app.tsx
  53. 1 1
      web/packages/shared/components/OverflowMenu/context.tsx
  54. 4 2
      web/packages/shared/components/OverflowMenu/menu.tsx
  55. 3 2
      web/packages/shared/components/RecoveryKey/index.tsx
  56. 3 1
      web/packages/shared/components/ThemeSwitcher.tsx
  57. 6 5
      web/packages/shared/components/VerifyMasterPasswordForm.tsx
  58. 1 1
      web/packages/shared/crypto/helpers.ts
  59. 1 0
      web/packages/shared/crypto/internal/libsodium.ts
  60. 2 3
      web/packages/shared/hooks/useLocalState.tsx
  61. 3 17
      web/packages/shared/storage/localStorage/index.ts
  62. 3 20
      web/packages/shared/storage/sessionStorage/index.ts
  63. 22 13
      web/packages/shared/themes/components.ts
  64. 1 0
      web/packages/shared/themes/mui-theme.d.ts
  65. 23 21
      web/packages/shared/themes/palette/index.tsx
  66. 0 3
      web/packages/shared/themes/types.ts
  67. 2 1
      web/packages/shared/tsconfig.json
  68. 6 0
      web/packages/shared/utils/index.ts

+ 7 - 10
web/apps/accounts/src/pages/_app.tsx

@@ -6,12 +6,9 @@ import { accountLogout } from "@ente/accounts/services/logout";
 import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
 import { Overlay } from "@ente/shared/components/Container";
 import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
-import type {
-    DialogBoxAttributesV2,
-    SetDialogBoxAttributesV2,
-} from "@ente/shared/components/DialogBoxV2/types";
+import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
 import EnteSpinner from "@ente/shared/components/EnteSpinner";
-import AppNavbar from "@ente/shared/components/Navbar/app";
+import { AppNavbar } from "@ente/shared/components/Navbar/app";
 import { useLocalState } from "@ente/shared/hooks/useLocalState";
 import HTTPService from "@ente/shared/network/HTTPService";
 import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@@ -28,7 +25,7 @@ import "styles/global.css";
 interface AppContextProps {
     isMobile: boolean;
     showNavBar: (show: boolean) => void;
-    setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
+    setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
     logout: () => void;
 }
 
@@ -39,8 +36,9 @@ export default function App({ Component, pageProps }: AppProps) {
 
     const [showNavbar, setShowNavBar] = useState(false);
 
-    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
-        useState<DialogBoxAttributesV2>();
+    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
+        DialogBoxAttributesV2 | undefined
+    >();
 
     const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
 
@@ -106,8 +104,7 @@ export default function App({ Component, pageProps }: AppProps) {
                     value={{
                         isMobile,
                         showNavBar,
-                        setDialogBoxAttributesV2:
-                            setDialogBoxAttributesV2 as any,
+                        setDialogBoxAttributesV2,
                         logout,
                     }}
                 >

+ 13 - 15
web/apps/auth/src/pages/_app.tsx

@@ -12,20 +12,16 @@ import {
 } from "@ente/shared/apps/constants";
 import { Overlay } from "@ente/shared/components/Container";
 import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
-import type {
-    DialogBoxAttributesV2,
-    SetDialogBoxAttributesV2,
-} from "@ente/shared/components/DialogBoxV2/types";
+import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
 import EnteSpinner from "@ente/shared/components/EnteSpinner";
 import { MessageContainer } from "@ente/shared/components/MessageContainer";
-import AppNavbar from "@ente/shared/components/Navbar/app";
+import { AppNavbar } from "@ente/shared/components/Navbar/app";
 import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
 import { useLocalState } from "@ente/shared/hooks/useLocalState";
 import HTTPService from "@ente/shared/network/HTTPService";
 import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
 import { getTheme } from "@ente/shared/themes";
 import { THEME_COLOR } from "@ente/shared/themes/constants";
-import type { SetTheme } from "@ente/shared/themes/types";
 import type { User } from "@ente/shared/user/types";
 import { CssBaseline, useMediaQuery } from "@mui/material";
 import { ThemeProvider } from "@mui/material/styles";
@@ -33,7 +29,7 @@ import { t } from "i18next";
 import type { AppProps } from "next/app";
 import { useRouter } from "next/router";
 import { createContext, useEffect, useRef, useState } from "react";
-import LoadingBar from "react-top-loading-bar";
+import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
 import "../../public/css/global.css";
 
 type AppContextType = {
@@ -42,13 +38,13 @@ type AppContextType = {
     finishLoading: () => void;
     isMobile: boolean;
     themeColor: THEME_COLOR;
-    setThemeColor: SetTheme;
+    setThemeColor: (themeColor: THEME_COLOR) => void;
     somethingWentWrong: () => void;
-    setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
+    setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
     logout: () => void;
 };
 
-export const AppContext = createContext<AppContextType>(null);
+export const AppContext = createContext<AppContextType | undefined>(undefined);
 
 export default function App({ Component, pageProps }: AppProps) {
     const router = useRouter();
@@ -58,10 +54,11 @@ export default function App({ Component, pageProps }: AppProps) {
         typeof window !== "undefined" && !window.navigator.onLine,
     );
     const [showNavbar, setShowNavBar] = useState(false);
-    const isLoadingBarRunning = useRef(false);
-    const loadingBar = useRef(null);
-    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
-        useState<DialogBoxAttributesV2>();
+    const isLoadingBarRunning = useRef<boolean>(false);
+    const loadingBar = useRef<LoadingBarRef>(null);
+    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
+        DialogBoxAttributesV2 | undefined
+    >();
     const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
     const isMobile = useMediaQuery("(max-width:428px)");
     const [themeColor, setThemeColor] = useLocalState(
@@ -134,9 +131,10 @@ export default function App({ Component, pageProps }: AppProps) {
         void accountLogout().then(() => router.push(PAGES.ROOT));
     };
 
+    // TODO: Refactor this to have a fallback
     const title = isI18nReady
         ? t("TITLE", { context: APPS.AUTH })
-        : APP_TITLES.get(APPS.AUTH);
+        : APP_TITLES.get(APPS.AUTH) ?? "";
 
     return (
         <>

+ 33 - 32
web/apps/auth/src/pages/auth.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import {
     HorizontalFlex,
     VerticallyCentered,
@@ -12,7 +13,7 @@ import { CustomError } from "@ente/shared/error";
 import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
 import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
 import MoreHoriz from "@mui/icons-material/MoreHoriz";
-import { Button, ButtonBase, Snackbar, TextField } from "@mui/material";
+import { Button, ButtonBase, Snackbar, TextField, styled } from "@mui/material";
 import { t } from "i18next";
 import { useRouter } from "next/router";
 import { AppContext } from "pages/_app";
@@ -20,20 +21,22 @@ import React, { useContext, useEffect, useState } from "react";
 import { generateOTPs, type Code } from "services/code";
 import { getAuthCodes } from "services/remote";
 
-const AuthenticatorCodesPage = () => {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     const router = useRouter();
-    const [codes, setCodes] = useState([]);
+    const [codes, setCodes] = useState<Code[]>([]);
     const [hasFetched, setHasFetched] = useState(false);
     const [searchTerm, setSearchTerm] = useState("");
 
     useEffect(() => {
         const fetchCodes = async () => {
             try {
-                const res = await getAuthCodes();
-                setCodes(res);
-            } catch (err) {
-                if (err.message === CustomError.KEY_MISSING) {
+                setCodes(await getAuthCodes());
+            } catch (e) {
+                if (
+                    e instanceof Error &&
+                    e.message == CustomError.KEY_MISSING
+                ) {
                     InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
                     router.push(PAGES.ROOT);
                 } else {
@@ -55,11 +58,9 @@ const AuthenticatorCodesPage = () => {
 
     if (!hasFetched) {
         return (
-            <>
-                <VerticallyCentered>
-                    <EnteSpinner></EnteSpinner>
-                </VerticallyCentered>
-            </>
+            <VerticallyCentered>
+                <EnteSpinner />
+            </VerticallyCentered>
         );
     }
 
@@ -77,7 +78,7 @@ const AuthenticatorCodesPage = () => {
                 }}
             >
                 <div style={{ marginBottom: "1rem" }} />
-                {filteredCodes.length === 0 && searchTerm.length === 0 ? (
+                {filteredCodes.length == 0 && searchTerm.length == 0 ? (
                     <></>
                 ) : (
                     <TextField
@@ -101,7 +102,7 @@ const AuthenticatorCodesPage = () => {
                         justifyContent: "center",
                     }}
                 >
-                    {filteredCodes.length === 0 ? (
+                    {filteredCodes.length == 0 ? (
                         <div
                             style={{
                                 alignItems: "center",
@@ -110,10 +111,10 @@ const AuthenticatorCodesPage = () => {
                                 marginTop: "32px",
                             }}
                         >
-                            {searchTerm.length !== 0 ? (
+                            {searchTerm.length > 0 ? (
                                 <p>{t("NO_RESULTS")}</p>
                             ) : (
-                                <div />
+                                <></>
                             )}
                         </div>
                     ) : (
@@ -122,18 +123,16 @@ const AuthenticatorCodesPage = () => {
                         ))
                     )}
                 </div>
-                <div style={{ marginBottom: "2rem" }} />
                 <Footer />
-                <div style={{ marginBottom: "4rem" }} />
             </div>
         </>
     );
 };
 
-export default AuthenticatorCodesPage;
+export default Page;
 
 const AuthNavbar: React.FC = () => {
-    const { isMobile, logout } = useContext(AppContext);
+    const { isMobile, logout } = ensure(useContext(AppContext));
 
     return (
         <NavbarBase isMobile={isMobile}>
@@ -158,11 +157,11 @@ const AuthNavbar: React.FC = () => {
     );
 };
 
-interface CodeDisplay {
+interface CodeDisplayProps {
     code: Code;
 }
 
-const CodeDisplay: React.FC<CodeDisplay> = ({ code }) => {
+const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
     const [otp, setOTP] = useState("");
     const [nextOTP, setNextOTP] = useState("");
     const [errorMessage, setErrorMessage] = useState("");
@@ -393,14 +392,7 @@ const UnparseableCode: React.FC<UnparseableCodeProps> = ({
 
 const Footer: React.FC = () => {
     return (
-        <div
-            style={{
-                display: "flex",
-                flexDirection: "column",
-                alignItems: "center",
-                justifyContent: "center",
-            }}
-        >
+        <Footer_>
             <p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
             <a
                 href="https://github.com/ente-io/ente/tree/main/auth#-download"
@@ -408,6 +400,15 @@ const Footer: React.FC = () => {
             >
                 <Button color="accent">{t("DOWNLOAD")}</Button>
             </a>
-        </div>
+        </Footer_>
     );
 };
+
+const Footer_ = styled("div")`
+    margin-block-start: 2rem;
+    margin-block-end: 4rem;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+`;

+ 7 - 4
web/apps/auth/src/pages/change-email.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import ChangeEmailPage from "@ente/accounts/pages/change-email";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function ChangeEmail() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <ChangeEmailPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/change-password.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import ChangePasswordPage from "@ente/accounts/pages/change-password";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function ChangePassword() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <ChangePasswordPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/credentials.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import CredentialPage from "@ente/accounts/pages/credentials";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Credential() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <CredentialPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/generate.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import GeneratePage from "@ente/accounts/pages/generate";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Generate() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <GeneratePage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 3 - 3
web/apps/auth/src/pages/index.tsx

@@ -1,8 +1,8 @@
 import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
 import { useRouter } from "next/router";
-import { useEffect } from "react";
+import React, { useEffect } from "react";
 
-const IndexPage = () => {
+const Page: React.FC = () => {
     const router = useRouter();
     useEffect(() => {
         router.push(PAGES.LOGIN);
@@ -11,4 +11,4 @@ const IndexPage = () => {
     return <></>;
 };
 
-export default IndexPage;
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/login.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import LoginPage from "@ente/accounts/pages/login";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Login() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <LoginPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 2 - 10
web/apps/auth/src/pages/passkeys/finish.tsx

@@ -1,11 +1,3 @@
-import PasskeysFinishPage from "@ente/accounts/pages/passkeys/finish";
+import Page from "@ente/accounts/pages/passkeys/finish";
 
-const PasskeysFinish = () => {
-    return (
-        <>
-            <PasskeysFinishPage />
-        </>
-    );
-};
-
-export default PasskeysFinish;
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/recover.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import RecoverPage from "@ente/accounts/pages/recover";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Recover() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <RecoverPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/signup.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import SignupPage from "@ente/accounts/pages/signup";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Sigup() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <SignupPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/two-factor/recover.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function TwoFactorRecover() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <TwoFactorRecoverPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/two-factor/setup.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function TwoFactorSetup() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <TwoFactorSetupPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/two-factor/verify.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
 import { APPS } from "@ente/shared/apps/constants";
-import { useContext } from "react";
+import React, { useContext } from "react";
 import { AppContext } from "../_app";
 
-export default function TwoFactorVerify() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <TwoFactorVerifyPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 7 - 4
web/apps/auth/src/pages/verify.tsx

@@ -1,9 +1,12 @@
+import { ensure } from "@/utils/ensure";
 import VerifyPage from "@ente/accounts/pages/verify";
 import { APPS } from "@ente/shared/apps/constants";
 import { AppContext } from "pages/_app";
-import { useContext } from "react";
+import React, { useContext } from "react";
 
-export default function Verify() {
-    const appContext = useContext(AppContext);
+const Page: React.FC = () => {
+    const appContext = ensure(useContext(AppContext));
     return <VerifyPage appContext={appContext} appName={APPS.AUTH} />;
-}
+};
+
+export default Page;

+ 3 - 3
web/apps/auth/src/services/code.ts

@@ -9,7 +9,7 @@ import { Steam } from "./steam";
  */
 export interface Code {
     /** A unique id for the corresponding "auth entity" in our system. */
-    id?: String;
+    id: string;
     /** The type of the code. */
     type: "totp" | "hotp" | "steam";
     /** The user's account or email for which this code is used. */
@@ -146,8 +146,8 @@ const parseIssuer = (url: URL, path: string): string => {
     let p = decodeURIComponent(path);
     if (p.startsWith("/")) p = p.slice(1);
 
-    if (p.includes(":")) p = p.split(":")[0];
-    else if (p.includes("-")) p = p.split("-")[0];
+    if (p.includes(":")) p = ensure(p.split(":")[0]);
+    else if (p.includes("-")) p = ensure(p.split("-")[0]);
 
     return p;
 };

+ 8 - 7
web/apps/auth/src/services/remote.ts

@@ -26,6 +26,9 @@ export const getAuthCodes = async (): Promise<Code[]> => {
             authEntity
                 .filter((f) => !f.isDeleted)
                 .map(async (entity) => {
+                    if (!entity.id) return undefined;
+                    if (!entity.encryptedData) return undefined;
+                    if (!entity.header) return undefined;
                     try {
                         const decryptedCode =
                             await cryptoWorker.decryptMetadata(
@@ -36,14 +39,12 @@ export const getAuthCodes = async (): Promise<Code[]> => {
                         return codeFromURIString(entity.id, decryptedCode);
                     } catch (e) {
                         log.error(`Failed to parse codeID ${entity.id}`, e);
-                        return null;
+                        return undefined;
                     }
                 }),
         );
-        // Remove null and undefined values
-        const filteredAuthCodes = authCodes.filter(
-            (f) => f !== null && f !== undefined,
-        );
+        // Remove undefined values
+        const filteredAuthCodes = authCodes.filter((f): f is Code => !!f);
         filteredAuthCodes.sort((a, b) => {
             if (a.issuer && b.issuer) {
                 return a.issuer.localeCompare(b.issuer);
@@ -58,7 +59,7 @@ export const getAuthCodes = async (): Promise<Code[]> => {
         });
         return filteredAuthCodes;
     } catch (e) {
-        if (e.message !== CustomError.AUTH_KEY_NOT_FOUND) {
+        if (e instanceof Error && e.message != CustomError.AUTH_KEY_NOT_FOUND) {
             log.error("get authenticator entities failed", e);
         }
         throw e;
@@ -92,7 +93,7 @@ export const getAuthKey = async (): Promise<AuthKey> => {
     } catch (e) {
         if (
             e instanceof ApiError &&
-            e.httpStatusCode === HttpStatusCode.NotFound
+            e.httpStatusCode == HttpStatusCode.NotFound
         ) {
             throw Error(CustomError.AUTH_KEY_NOT_FOUND);
         } else {

+ 6 - 11
web/apps/auth/tsconfig.json

@@ -3,23 +3,18 @@
     "include": [
         "src",
         "next-env.d.ts",
+        "../../packages/next/global-electron.d.ts",
         "../../packages/shared/themes/mui-theme.d.ts"
     ],
-    // Temporarily disable some things to get the existing code to compile
-    // without warnings.
     "compilerOptions": {
+        /* Set the base directory from which to resolve bare module names */
         "baseUrl": "./src",
 
-        "jsxImportSource": "@emotion/react",
-
-        "strict": false,
-        /* Stricter than strict */
-        "noImplicitReturns": false,
-        "noUnusedParameters": false,
-        "noUnusedLocals": false,
-        "noFallthroughCasesInSwitch": false,
-        /* e.g. makes array indexing returns undefined */
+        /* This is hard to enforce in certain cases where we do a lot of array
+           indexing, e.g. image/ML ops, and TS doesn't currently have a way to
+           disable this for blocks of code. */
         "noUncheckedIndexedAccess": false,
+        /* MUI doesn't play great with exactOptionalPropertyTypes currently. */
         "exactOptionalPropertyTypes": false
     }
 }

+ 5 - 4
web/apps/photos/src/components/Sidebar/index.tsx

@@ -157,9 +157,9 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
 }) => {
     const galleryContext = useContext(GalleryContext);
 
-    const [userDetails, setUserDetails] = useLocalState<UserDetails>(
-        LS_KEYS.USER_DETAILS,
-    );
+    const [userDetails, setUserDetails] = useLocalState<
+        UserDetails | undefined
+    >(LS_KEYS.USER_DETAILS, undefined);
     const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
         useState(false);
 
@@ -198,6 +198,7 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
             openMemberSubscriptionManage();
         } else {
             if (
+                userDetails &&
                 hasStripeSubscription(userDetails.subscription) &&
                 isSubscriptionPastDue(userDetails.subscription)
             ) {
@@ -529,7 +530,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
         });
 
     const toggleTheme = () => {
-        setThemeColor((themeColor) =>
+        setThemeColor(
             themeColor === THEME_COLOR.DARK
                 ? THEME_COLOR.LIGHT
                 : THEME_COLOR.DARK,

+ 1 - 1
web/apps/photos/src/components/Upload/CollectionMappingChoiceModal.tsx

@@ -22,7 +22,7 @@ export const CollectionMappingChoiceModal: React.FC<
 
     return (
         <Dialog open={open} onClose={handleClose}>
-            <DialogTitleWithCloseButton onClose={handleClose}>
+            <DialogTitleWithCloseButton onClose={onClose}>
                 {t("MULTI_FOLDER_UPLOAD")}
             </DialogTitleWithCloseButton>
             <DialogContent>

+ 9 - 6
web/apps/photos/src/components/WatchFolder.tsx

@@ -119,12 +119,11 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
                 onClose={onClose}
                 PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
             >
-                <DialogTitleWithCloseButton
-                    onClose={onClose}
-                    sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
-                >
-                    {t("WATCHED_FOLDERS")}
-                </DialogTitleWithCloseButton>
+                <Title_>
+                    <DialogTitleWithCloseButton onClose={onClose}>
+                        {t("WATCHED_FOLDERS")}
+                    </DialogTitleWithCloseButton>
+                </Title_>
                 <DialogContent sx={{ flex: 1 }}>
                     <Stack spacing={1} p={1.5} height={"100%"}>
                         <WatchList {...{ watches, removeWatch }} />
@@ -149,6 +148,10 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
     );
 };
 
+const Title_ = styled("div")`
+    padding: 32px 16px 16px 24px;
+`;
+
 interface WatchList {
     watches: FolderWatch[];
     removeWatch: (watch: FolderWatch) => void;

+ 7 - 10
web/apps/photos/src/pages/_app.tsx

@@ -18,13 +18,10 @@ import {
     SetDialogBoxAttributes,
 } from "@ente/shared/components/DialogBox/types";
 import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
-import type {
-    DialogBoxAttributesV2,
-    SetDialogBoxAttributesV2,
-} from "@ente/shared/components/DialogBoxV2/types";
+import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
 import EnteSpinner from "@ente/shared/components/EnteSpinner";
 import { MessageContainer } from "@ente/shared/components/MessageContainer";
-import AppNavbar from "@ente/shared/components/Navbar/app";
+import { AppNavbar } from "@ente/shared/components/Navbar/app";
 import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
 import { useLocalState } from "@ente/shared/hooks/useLocalState";
 import HTTPService from "@ente/shared/network/HTTPService";
@@ -36,7 +33,6 @@ import {
 } from "@ente/shared/storage/localStorage/helpers";
 import { getTheme } from "@ente/shared/themes";
 import { THEME_COLOR } from "@ente/shared/themes/constants";
-import type { SetTheme } from "@ente/shared/themes/types";
 import type { User } from "@ente/shared/user/types";
 import ArrowForward from "@mui/icons-material/ArrowForward";
 import { CssBaseline, useMediaQuery } from "@mui/material";
@@ -95,9 +91,9 @@ type AppContextType = {
     setWatchFolderFiles: (files: FileList) => void;
     isMobile: boolean;
     themeColor: THEME_COLOR;
-    setThemeColor: SetTheme;
+    setThemeColor: (themeColor: THEME_COLOR) => void;
     somethingWentWrong: () => void;
-    setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
+    setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
     isCFProxyDisabled: boolean;
     setIsCFProxyDisabled: (disabled: boolean) => void;
     logout: () => void;
@@ -119,8 +115,9 @@ export default function App({ Component, pageProps }: AppProps) {
     const isLoadingBarRunning = useRef(false);
     const loadingBar = useRef(null);
     const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
-    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
-        useState<DialogBoxAttributesV2>();
+    const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
+        DialogBoxAttributesV2 | undefined
+    >();
     useState<DialogBoxAttributes>(null);
     const [messageDialogView, setMessageDialogView] = useState(false);
     const [dialogBoxV2View, setDialogBoxV2View] = useState(false);

+ 82 - 76
web/apps/photos/src/pages/index.tsx

@@ -25,81 +25,6 @@ import { useContext, useEffect, useState } from "react";
 import { Trans } from "react-i18next";
 import { AppContext } from "./_app";
 
-const Container = styled("div")`
-    display: flex;
-    flex: 1;
-    align-items: center;
-    justify-content: center;
-    background-color: #000;
-
-    @media (max-width: 1024px) {
-        flex-direction: column;
-    }
-`;
-
-const SlideContainer = styled("div")`
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    text-align: center;
-
-    @media (max-width: 1024px) {
-        flex-grow: 0;
-    }
-`;
-
-const DesktopBox = styled("div")`
-    flex: 1;
-    height: 100%;
-    padding: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    background-color: #242424;
-
-    @media (max-width: 1024px) {
-        display: none;
-    }
-`;
-
-const MobileBox = styled("div")`
-    display: none;
-
-    @media (max-width: 1024px) {
-        max-width: 375px;
-        width: 100%;
-        padding: 12px;
-        display: flex;
-        flex-direction: column;
-        gap: 8px;
-    }
-`;
-
-const SideBox = styled("div")`
-    display: flex;
-    flex-direction: column;
-    min-width: 320px;
-`;
-
-const TextContainer = (props: TypographyProps) => (
-    <Typography color={"text.muted"} mt={2} mb={3} {...props} />
-);
-
-const FeatureText = (props: TypographyProps) => (
-    <Typography variant="h3" mt={4} {...props} />
-);
-
-const Img = styled("img")`
-    height: 250px;
-    object-fit: contain;
-
-    @media (max-width: 400px) {
-        height: 180px;
-    }
-`;
-
 export default function LandingPage() {
     const router = useRouter();
     const appContext = useContext(AppContext);
@@ -189,7 +114,9 @@ export default function LandingPage() {
             ) : (
                 <>
                     <SlideContainer>
-                        <EnteLogo height={24} sx={{ mb: 8 }} />
+                        <Logo_>
+                            <EnteLogo height={24} />
+                        </Logo_>
                         <Slideshow />
                     </SlideContainer>
                     <MobileBox>
@@ -223,6 +150,68 @@ export default function LandingPage() {
     );
 }
 
+const Container = styled("div")`
+    display: flex;
+    flex: 1;
+    align-items: center;
+    justify-content: center;
+    background-color: #000;
+
+    @media (max-width: 1024px) {
+        flex-direction: column;
+    }
+`;
+
+const SlideContainer = styled("div")`
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+
+    @media (max-width: 1024px) {
+        flex-grow: 0;
+    }
+`;
+
+const Logo_ = styled("div")`
+    margin-block-end: 64px;
+`;
+
+const DesktopBox = styled("div")`
+    flex: 1;
+    height: 100%;
+    padding: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #242424;
+
+    @media (max-width: 1024px) {
+        display: none;
+    }
+`;
+
+const MobileBox = styled("div")`
+    display: none;
+
+    @media (max-width: 1024px) {
+        max-width: 375px;
+        width: 100%;
+        padding: 12px;
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+    }
+`;
+
+const SideBox = styled("div")`
+    display: flex;
+    flex-direction: column;
+    min-width: 320px;
+`;
+
 const Slideshow: React.FC = () => {
     return (
         <CarouselProvider
@@ -276,6 +265,23 @@ const Slideshow: React.FC = () => {
     );
 };
 
+const TextContainer = (props: TypographyProps) => (
+    <Typography color={"text.muted"} mt={2} mb={3} {...props} />
+);
+
+const FeatureText = (props: TypographyProps) => (
+    <Typography variant="h3" mt={4} {...props} />
+);
+
+const Img = styled("img")`
+    height: 250px;
+    object-fit: contain;
+
+    @media (max-width: 400px) {
+        height: 180px;
+    }
+`;
+
 const CustomDotGroup = styled(DotGroup)`
     margin-block-start: 2px;
     margin-block-end: 24px;

+ 1 - 1
web/packages/accounts/api/srp.ts

@@ -127,7 +127,7 @@ export const updateSRPAndKeys = async (
         const resp = await HTTPService.post(
             `${ENDPOINT}/users/srp/update`,
             updateSRPAndKeyRequest,
-            null,
+            undefined,
             {
                 "X-Auth-Token": token,
             },

+ 16 - 15
web/packages/accounts/api/user.ts

@@ -66,14 +66,10 @@ export const logout = async () => {
 };
 
 export const verifyTwoFactor = async (code: string, sessionID: string) => {
-    const resp = await HTTPService.post(
-        `${ENDPOINT}/users/two-factor/verify`,
-        {
-            code,
-            sessionID,
-        },
-        null,
-    );
+    const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
+        code,
+        sessionID,
+    });
     return resp.data as UserVerificationResponse;
 };
 
@@ -108,7 +104,7 @@ export const changeEmail = async (email: string, ott: string) => {
             email,
             ott,
         },
-        null,
+        undefined,
         {
             "X-Auth-Token": getToken(),
         },
@@ -127,7 +123,7 @@ export const setupTwoFactor = async () => {
     const resp = await HTTPService.post(
         `${ENDPOINT}/users/two-factor/setup`,
         null,
-        null,
+        undefined,
         {
             "X-Auth-Token": getToken(),
         },
@@ -148,7 +144,7 @@ export const enableTwoFactor = async (
             twoFactorSecretDecryptionNonce:
                 recoveryEncryptedTwoFactorSecret.nonce,
         },
-        null,
+        undefined,
         {
             "X-Auth-Token": getToken(),
         },
@@ -156,12 +152,17 @@ export const enableTwoFactor = async (
 };
 
 export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
-    HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
+    HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, undefined, {
         "X-Auth-Token": token,
     });
 
 export const disableTwoFactor = async () => {
-    await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
-        "X-Auth-Token": getToken(),
-    });
+    await HTTPService.post(
+        `${ENDPOINT}/users/two-factor/disable`,
+        null,
+        undefined,
+        {
+            "X-Auth-Token": getToken(),
+        },
+    );
 };

+ 28 - 15
web/packages/accounts/components/ChangeEmail.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import { wait } from "@/utils/promise";
 import { changeEmail, sendOTTForEmailChange } from "@ente/accounts/api/user";
 import { APP_HOMES } from "@ente/shared/apps/constants";
@@ -11,7 +12,7 @@ import { Alert, Box, TextField } from "@mui/material";
 import { Formik, type FormikHelpers } from "formik";
 import { t } from "i18next";
 import { useRouter } from "next/router";
-import { useRef, useState } from "react";
+import { useState } from "react";
 import { Trans } from "react-i18next";
 import * as Yup from "yup";
 
@@ -23,8 +24,7 @@ interface formValues {
 function ChangeEmailForm({ appName }: PageProps) {
     const [loading, setLoading] = useState(false);
     const [ottInputVisible, setShowOttInputVisibility] = useState(false);
-    const ottInputRef = useRef(null);
-    const [email, setEmail] = useState(null);
+    const [email, setEmail] = useState<string | null>(null);
     const [showMessage, setShowMessage] = useState(false);
     const [success, setSuccess] = useState(false);
 
@@ -40,9 +40,11 @@ function ChangeEmailForm({ appName }: PageProps) {
             setEmail(email);
             setShowOttInputVisibility(true);
             setShowMessage(true);
-            setTimeout(() => {
-                ottInputRef.current?.focus();
-            }, 250);
+            // TODO: What was this meant to focus on? The ref referred to an
+            // Form element that was removed. Is this still needed.
+            // setTimeout(() => {
+            //     ottInputRef.current?.focus();
+            // }, 250);
         } catch (e) {
             setFieldError("email", t("EMAIl_ALREADY_OWNED"));
         }
@@ -55,7 +57,7 @@ function ChangeEmailForm({ appName }: PageProps) {
     ) => {
         try {
             setLoading(true);
-            await changeEmail(email, ott);
+            await changeEmail(email, ensure(ott));
             setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
             setLoading(false);
             setSuccess(true);
@@ -68,18 +70,27 @@ function ChangeEmailForm({ appName }: PageProps) {
     };
 
     const goToApp = () => {
-        router.push(APP_HOMES.get(appName));
+        // TODO: Refactor the type of APP_HOMES to not require the ??
+        router.push(APP_HOMES.get(appName) ?? "/");
     };
 
     return (
         <Formik<formValues>
             initialValues={{ email: "" }}
-            validationSchema={Yup.object().shape({
-                email: Yup.string()
-                    .email(t("EMAIL_ERROR"))
-                    .required(t("REQUIRED")),
-                ott: ottInputVisible && Yup.string().required(t("REQUIRED")),
-            })}
+            validationSchema={
+                ottInputVisible
+                    ? Yup.object().shape({
+                          email: Yup.string()
+                              .email(t("EMAIL_ERROR"))
+                              .required(t("REQUIRED")),
+                          ott: Yup.string().required(t("REQUIRED")),
+                      })
+                    : Yup.object().shape({
+                          email: Yup.string()
+                              .email(t("EMAIL_ERROR"))
+                              .required(t("REQUIRED")),
+                      })
+            }
             validateOnChange={false}
             validateOnBlur={false}
             onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}
@@ -148,7 +159,9 @@ function ChangeEmailForm({ appName }: PageProps) {
 
                     <FormPaperFooter
                         style={{
-                            justifyContent: ottInputVisible && "space-between",
+                            justifyContent: ottInputVisible
+                                ? "space-between"
+                                : "normal",
                         }}
                     >
                         {ottInputVisible && (

+ 2 - 1
web/packages/accounts/components/SetPasswordForm.tsx

@@ -58,7 +58,8 @@ function SetPasswordForm(props: SetPasswordFormProps) {
                 setFieldError("confirm", t("PASSPHRASE_MATCH_ERROR"));
             }
         } catch (e) {
-            setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${e.message}`);
+            const message = e instanceof Error ? e.message : "";
+            setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${message}`);
         } finally {
             setLoading(false);
         }

+ 2 - 1
web/packages/accounts/components/SignUp.tsx

@@ -84,7 +84,8 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
                 setLocalReferralSource(referral);
                 await sendOtt(appName, email);
             } catch (e) {
-                setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${e.message}`);
+                const message = e instanceof Error ? e.message : "";
+                setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${message}`);
                 throw e;
             }
             try {

+ 3 - 2
web/packages/accounts/components/two-factor/VerifyForm.tsx

@@ -26,7 +26,7 @@ export type VerifyTwoFactorCallback = (
 
 export default function VerifyTwoFactor(props: Props) {
     const [waiting, setWaiting] = useState(false);
-    const otpInputRef = useRef(null);
+    const otpInputRef = useRef<OtpInput>(null);
     const [success, setSuccess] = useState(false);
 
     const markSuccessful = async () => {
@@ -47,7 +47,8 @@ export default function VerifyTwoFactor(props: Props) {
             for (let i = 0; i < 6; i++) {
                 otpInputRef.current?.focusPrevInput();
             }
-            setFieldError("otp", `${t("UNKNOWN_ERROR")} ${e.message}`);
+            const message = e instanceof Error ? e.message : "";
+            setFieldError("otp", `${t("UNKNOWN_ERROR")} ${message}`);
         }
         setWaiting(false);
     };

+ 2 - 2
web/packages/accounts/components/two-factor/setup/ManualMode.tsx

@@ -6,7 +6,7 @@ import { t } from "i18next";
 import LinkButton from "@ente/shared/components/LinkButton";
 
 interface Iprops {
-    twoFactorSecret: TwoFactorSecret;
+    twoFactorSecret?: TwoFactorSecret;
     changeToQRMode: () => void;
 }
 export default function SetupManualMode({
@@ -16,7 +16,7 @@ export default function SetupManualMode({
     return (
         <>
             <Typography>{t("TWO_FACTOR_MANUAL_CODE_INSTRUCTION")}</Typography>
-            <CodeBlock code={twoFactorSecret?.secretCode} my={2} />
+            <CodeBlock code={twoFactorSecret?.secretCode ?? ""} my={2} />
             <LinkButton onClick={changeToQRMode}>
                 {t("SCAN_QR_CODE")}
             </LinkButton>

+ 1 - 1
web/packages/accounts/components/two-factor/setup/QRMode.tsx

@@ -7,7 +7,7 @@ import { Typography } from "@mui/material";
 import { LoadingQRCode, QRCode } from "../styledComponents";
 
 interface Iprops {
-    twoFactorSecret: TwoFactorSecret;
+    twoFactorSecret?: TwoFactorSecret;
     changeToManualMode: () => void;
 }
 

+ 1 - 1
web/packages/accounts/components/two-factor/setup/index.tsx

@@ -6,7 +6,7 @@ import { VerticallyCentered } from "@ente/shared/components/Container";
 import { useState } from "react";
 
 interface Iprops {
-    twoFactorSecret: TwoFactorSecret;
+    twoFactorSecret?: TwoFactorSecret;
 }
 export function TwoFactorSetup({ twoFactorSecret }: Iprops) {
     const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);

+ 7 - 4
web/packages/accounts/pages/change-password.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import { startSRPSetup, updateSRPAndKeys } from "@ente/accounts/api/srp";
 import SetPasswordForm, {
     type SetPasswordFormProps,
@@ -91,7 +92,7 @@ export default function ChangePassword({ appName }: PageProps) {
 
         const srpA = convertBufferToBase64(srpClient.computeA());
 
-        const { setupID, srpB } = await startSRPSetup(token, {
+        const { setupID, srpB } = await startSRPSetup(ensure(token), {
             srpUserID,
             srpSalt,
             srpVerifier,
@@ -102,7 +103,7 @@ export default function ChangePassword({ appName }: PageProps) {
 
         const srpM1 = convertBufferToBase64(srpClient.computeM1());
 
-        await updateSRPAndKeys(token, {
+        await updateSRPAndKeys(ensure(token), {
             setupID,
             srpM1,
             updatedKeyAttr: updatedKey,
@@ -121,15 +122,17 @@ export default function ChangePassword({ appName }: PageProps) {
 
     const redirectToAppHome = () => {
         setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
-        router.push(APP_HOMES.get(appName));
+        // TODO: Refactor the type of APP_HOMES to not require the ??
+        router.push(APP_HOMES.get(appName) ?? "/");
     };
 
+    // TODO: Handle the case where user is not loaded yet.
     return (
         <VerticallyCentered>
             <FormPaper>
                 <FormPaperTitle>{t("CHANGE_PASSWORD")}</FormPaperTitle>
                 <SetPasswordForm
-                    userEmail={user?.email}
+                    userEmail={user?.email ?? ""}
                     callback={onSubmit}
                     buttonText={t("CHANGE_PASSWORD")}
                 />

+ 16 - 8
web/packages/accounts/pages/credentials.tsx

@@ -1,5 +1,6 @@
 import { isDevBuild } from "@/next/env";
 import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
 import { APP_HOMES } from "@ente/shared/apps/constants";
 import type { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
@@ -86,7 +87,8 @@ export default function Credentials({ appContext, appName }: PageProps) {
             }
             const token = getToken();
             if (key && token) {
-                router.push(APP_HOMES.get(appName));
+                // TODO: Refactor the type of APP_HOMES to not require the ??
+                router.push(APP_HOMES.get(appName) ?? "/");
                 return;
             }
             const kekEncryptedAttributes: B64EncryptionResult = getKey(
@@ -148,7 +150,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
                     id,
                     twoFactorSessionID,
                     passkeySessionID,
-                } = await loginViaSRP(srpAttributes, kek);
+                } = await loginViaSRP(ensure(srpAttributes), kek);
                 setIsFirstLogin(true);
                 if (passkeySessionID) {
                     const sessionKeyAttributes =
@@ -168,7 +170,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
                     window.location.href = `${getAccountsURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
                         window.location.origin
                     }/passkeys/finish`;
-                    return;
+                    return undefined;
                 } else if (twoFactorSessionID) {
                     const sessionKeyAttributes =
                         await cryptoWorker.generateKeyAndEncryptToB64(kek);
@@ -193,11 +195,15 @@ export default function Credentials({ appContext, appName }: PageProps) {
                         id,
                         isTwoFactorEnabled: false,
                     });
-                    setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
+                    if (keyAttributes)
+                        setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
                     return keyAttributes;
                 }
             } catch (e) {
-                if (e.message !== CustomError.TWO_FACTOR_ENABLED) {
+                if (
+                    e instanceof Error &&
+                    e.message != CustomError.TWO_FACTOR_ENABLED
+                ) {
                     log.error("getKeyAttributes failed", e);
                 }
                 throw e;
@@ -221,10 +227,10 @@ export default function Credentials({ appContext, appName }: PageProps) {
             await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
             await decryptAndStoreToken(keyAttributes, key);
             try {
-                let srpAttributes: SRPAttributes = getData(
+                let srpAttributes: SRPAttributes | null = getData(
                     LS_KEYS.SRP_ATTRIBUTES,
                 );
-                if (!srpAttributes) {
+                if (!srpAttributes && user) {
                     srpAttributes = await getSRPAttributes(user.email);
                     if (srpAttributes) {
                         setData(LS_KEYS.SRP_ATTRIBUTES, srpAttributes);
@@ -258,10 +264,12 @@ export default function Credentials({ appContext, appName }: PageProps) {
         );
     }
 
+    // TODO: Handle the case when user is not present, or exclude that
+    // possibility using types.
     return (
         <VerticallyCentered>
             <FormPaper style={{ minWidth: "320px" }}>
-                <Header>{user.email}</Header>
+                <Header>{user?.email ?? ""}</Header>
 
                 <VerifyMasterPasswordForm
                     buttonText={t("VERIFY_PASSPHRASE")}

+ 27 - 18
web/packages/accounts/pages/generate.tsx

@@ -1,18 +1,12 @@
 import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
 import { putAttributes } from "@ente/accounts/api/user";
+import SetPasswordForm, {
+    type SetPasswordFormProps,
+} from "@ente/accounts/components/SetPasswordForm";
+import { PAGES } from "@ente/accounts/constants/pages";
 import { configureSRP } from "@ente/accounts/services/srp";
 import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
-import {
-    generateAndSaveIntermediateKeyAttributes,
-    saveKeyInSessionStore,
-} from "@ente/shared/crypto/helpers";
-import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
-import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage";
-import { t } from "i18next";
-import { useEffect, useState } from "react";
-
-import SetPasswordForm from "@ente/accounts/components/SetPasswordForm";
-import { PAGES } from "@ente/accounts/constants/pages";
 import { APP_HOMES } from "@ente/shared/apps/constants";
 import type { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
@@ -22,12 +16,20 @@ import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
 import FormTitle from "@ente/shared/components/Form/FormPaper/Title";
 import LinkButton from "@ente/shared/components/LinkButton";
 import RecoveryKey from "@ente/shared/components/RecoveryKey";
+import {
+    generateAndSaveIntermediateKeyAttributes,
+    saveKeyInSessionStore,
+} from "@ente/shared/crypto/helpers";
+import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
 import {
     justSignedUp,
     setJustSignedUp,
 } from "@ente/shared/storage/localStorage/helpers";
+import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage";
 import type { KeyAttributes, User } from "@ente/shared/user/types";
+import { t } from "i18next";
 import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
 
 export default function Generate({ appContext, appName }: PageProps) {
     const { logout } = appContext;
@@ -54,7 +56,8 @@ export default function Generate({ appContext, appName }: PageProps) {
                     setRecoveryModalView(true);
                     setLoading(false);
                 } else {
-                    router.push(APP_HOMES.get(appName));
+                    // TODO: Refactor the type of APP_HOMES to not require the ??
+                    router.push(APP_HOMES.get(appName) ?? "/");
                 }
             } else if (keyAttributes?.encryptedKey) {
                 router.push(PAGES.CREDENTIALS);
@@ -67,12 +70,16 @@ export default function Generate({ appContext, appName }: PageProps) {
         appContext.showNavBar(true);
     }, []);
 
-    const onSubmit = async (passphrase, setFieldError) => {
+    const onSubmit: SetPasswordFormProps["callback"] = async (
+        passphrase,
+        setFieldError,
+    ) => {
         try {
             const { keyAttributes, masterKey, srpSetupAttributes } =
                 await generateKeyAndSRPAttributes(passphrase);
 
-            await putAttributes(token, keyAttributes);
+            // TODO: Refactor the code to not require this ensure
+            await putAttributes(ensure(token), keyAttributes);
             await configureSRP(srpSetupAttributes);
             await generateAndSaveIntermediateKeyAttributes(
                 passphrase,
@@ -90,7 +97,7 @@ export default function Generate({ appContext, appName }: PageProps) {
 
     return (
         <>
-            {loading ? (
+            {loading || !user ? (
                 <VerticallyCentered>
                     <EnteSpinner />
                 </VerticallyCentered>
@@ -100,16 +107,18 @@ export default function Generate({ appContext, appName }: PageProps) {
                     show={recoverModalView}
                     onHide={() => {
                         setRecoveryModalView(false);
-                        router.push(APP_HOMES.get(appName));
+                        // TODO: Refactor the type of APP_HOMES to not require the ??
+                        router.push(APP_HOMES.get(appName) ?? "/");
                     }}
-                    somethingWentWrong={() => null}
+                    /* TODO: Why is this error being ignored */
+                    somethingWentWrong={() => {}}
                 />
             ) : (
                 <VerticallyCentered>
                     <FormPaper>
                         <FormTitle>{t("SET_PASSPHRASE")}</FormTitle>
                         <SetPasswordForm
-                            userEmail={user?.email}
+                            userEmail={user.email}
                             callback={onSubmit}
                             buttonText={t("SET_PASSPHRASE")}
                         />

+ 10 - 5
web/packages/accounts/pages/recover.tsx

@@ -1,4 +1,5 @@
 import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
 import { sendOtt } from "@ente/accounts/api/user";
 import { PAGES } from "@ente/accounts/constants/pages";
 import { APP_HOMES } from "@ente/shared/apps/constants";
@@ -29,7 +30,9 @@ const bip39 = require("bip39");
 bip39.setDefaultWordlist("english");
 
 export default function Recover({ appContext, appName }: PageProps) {
-    const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
+    const [keyAttributes, setKeyAttributes] = useState<
+        KeyAttributes | undefined
+    >();
 
     const router = useRouter();
 
@@ -50,7 +53,8 @@ export default function Recover({ appContext, appName }: PageProps) {
         if (!keyAttributes) {
             router.push(PAGES.GENERATE);
         } else if (key) {
-            router.push(APP_HOMES.get(appName));
+            // TODO: Refactor the type of APP_HOMES to not require the ??
+            router.push(APP_HOMES.get(appName) ?? "/");
         } else {
             setKeyAttributes(keyAttributes);
         }
@@ -76,13 +80,14 @@ export default function Recover({ appContext, appName }: PageProps) {
                 recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
             }
             const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+            const keyAttr = ensure(keyAttributes);
             const masterKey = await cryptoWorker.decryptB64(
-                keyAttributes.masterKeyEncryptedWithRecoveryKey,
-                keyAttributes.masterKeyDecryptionNonce,
+                keyAttr.masterKeyEncryptedWithRecoveryKey,
+                keyAttr.masterKeyDecryptionNonce,
                 await cryptoWorker.fromHex(recoveryKey),
             );
             await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
-            await decryptAndStoreToken(keyAttributes, masterKey);
+            await decryptAndStoreToken(keyAttr, masterKey);
 
             setData(LS_KEYS.SHOW_BACK_BUTTON, { value: false });
             router.push(PAGES.CHANGE_PASSWORD);

+ 7 - 6
web/packages/accounts/pages/two-factor/recover.tsx

@@ -1,4 +1,5 @@
 import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
 import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
 import { PAGES } from "@ente/accounts/constants/pages";
 import { TwoFactorType } from "@ente/accounts/constants/twofactor";
@@ -35,8 +36,8 @@ export default function Recover({
     const { logout } = appContext;
 
     const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
-        useState<B64EncryptionResult>(null);
-    const [sessionID, setSessionID] = useState(null);
+        useState<Omit<B64EncryptionResult, "key"> | null>(null);
+    const [sessionID, setSessionID] = useState<string | null>(null);
     const [doesHaveEncryptedRecoveryKey, setDoesHaveEncryptedRecoveryKey] =
         useState(false);
 
@@ -70,7 +71,6 @@ export default function Recover({
                     setEncryptedTwoFactorSecret({
                         encryptedData: resp.encryptedSecret,
                         nonce: resp.secretDecryptionNonce,
-                        key: null,
                     });
                 }
             } catch (e) {
@@ -111,13 +111,14 @@ export default function Recover({
                 recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
             }
             const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+            const { encryptedData, nonce } = ensure(encryptedTwoFactorSecret);
             const twoFactorSecret = await cryptoWorker.decryptB64(
-                encryptedTwoFactorSecret.encryptedData,
-                encryptedTwoFactorSecret.nonce,
+                encryptedData,
+                nonce,
                 await cryptoWorker.fromHex(recoveryKey),
             );
             const resp = await removeTwoFactor(
-                sessionID,
+                ensure(sessionID),
                 twoFactorSecret,
                 twoFactorType,
             );

+ 7 - 4
web/packages/accounts/pages/two-factor/setup.tsx

@@ -1,4 +1,5 @@
 import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
 import { enableTwoFactor, setupTwoFactor } from "@ente/accounts/api/user";
 import VerifyTwoFactor, {
     type VerifyTwoFactorCallback,
@@ -23,8 +24,9 @@ export enum SetupMode {
 }
 
 export default function SetupTwoFactor({ appName }: PageProps) {
-    const [twoFactorSecret, setTwoFactorSecret] =
-        useState<TwoFactorSecret>(null);
+    const [twoFactorSecret, setTwoFactorSecret] = useState<
+        TwoFactorSecret | undefined
+    >();
 
     const router = useRouter();
 
@@ -48,7 +50,7 @@ export default function SetupTwoFactor({ appName }: PageProps) {
         markSuccessful,
     ) => {
         const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(
-            twoFactorSecret.secretCode,
+            ensure(twoFactorSecret).secretCode,
         );
         await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
         await markSuccessful();
@@ -56,7 +58,8 @@ export default function SetupTwoFactor({ appName }: PageProps) {
             ...getData(LS_KEYS.USER),
             isTwoFactorEnabled: true,
         });
-        router.push(APP_HOMES.get(appName));
+        // TODO: Refactor the type of APP_HOMES to not require the ??
+        router.push(APP_HOMES.get(appName) ?? "/");
     };
 
     return (

+ 2 - 1
web/packages/accounts/pages/two-factor/verify.tsx

@@ -4,6 +4,7 @@ import VerifyTwoFactor, {
 } from "@ente/accounts/components/two-factor/VerifyForm";
 import { PAGES } from "@ente/accounts/constants/pages";
 
+import { ensure } from "@/utils/ensure";
 import type { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
 import FormPaper from "@ente/shared/components/Form/FormPaper";
@@ -55,7 +56,7 @@ export const TwoFactorVerify: React.FC<PageProps> = ({
                 encryptedToken,
                 id,
             });
-            setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
+            setData(LS_KEYS.KEY_ATTRIBUTES, ensure(keyAttributes));
             const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL);
             InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
             router.push(redirectURL ?? PAGES.CREDENTIALS);

+ 2 - 1
web/packages/accounts/pages/verify.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import type { UserVerificationResponse } from "@ente/accounts/types/user";
 import type { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
@@ -110,7 +111,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
                 } else {
                     if (getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)) {
                         await putAttributes(
-                            token,
+                            ensure(token),
                             getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES),
                         );
                     }

+ 8 - 2
web/packages/build-config/tsconfig-next.json

@@ -3,6 +3,12 @@
     "extends": "@/build-config/tsconfig-typecheck.json",
     "compilerOptions": {
         /* Also indicate expectation of a WebWorker runtime */
-        "lib": ["ESnext", "DOM", "DOM.Iterable", "WebWorker"]
-    }
+        "lib": ["ESnext", "DOM", "DOM.Iterable", "WebWorker"],
+
+        /* Next.js insists on adding these. Sigh. */
+        "allowJs": true,
+        "incremental": true
+    },
+    /* Next.js insists on adding this, even though we don't need it. */
+    "exclude": ["node_modules"]
 }

+ 3 - 1
web/packages/build-config/tsconfig-typecheck.json

@@ -67,8 +67,10 @@
         "noUnusedParameters": true,
         "noUnusedLocals": true,
         "noFallthroughCasesInSwitch": true,
-        /* e.g. makes array indexing returns undefined */
+        /* e.g. makes array indexing returns undefined. */
         "noUncheckedIndexedAccess": true,
+        /* Treat optional (?) properties and properties where undefined is a
+           valid value separately */
         "exactOptionalPropertyTypes": true
     }
 }

+ 2 - 2
web/packages/shared/apps/types.ts

@@ -1,12 +1,12 @@
 import { TwoFactorType } from "@ente/accounts/constants/twofactor";
-import type { SetDialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
+import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
 import { APPS } from "./constants";
 
 export interface PageProps {
     appContext: {
         showNavBar: (show: boolean) => void;
         isMobile: boolean;
-        setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
+        setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
         logout: () => void;
     };
     appName: APPS;

+ 1 - 1
web/packages/shared/components/CodeBlock/index.tsx

@@ -6,7 +6,7 @@ import CopyButton from "./CopyButton";
 import { CodeWrapper, CopyButtonWrapper, Wrapper } from "./styledComponents";
 
 type Iprops = React.PropsWithChildren<{
-    code: string;
+    code: string | null;
     wordBreak?: "normal" | "break-all" | "keep-all" | "break-word";
 }>;
 

+ 8 - 3
web/packages/shared/components/DialogBox/TitleWithCloseButton.tsx

@@ -6,12 +6,17 @@ import {
     Typography,
     type DialogProps,
 } from "@mui/material";
+import React from "react";
 
-const DialogTitleWithCloseButton = (props) => {
-    const { children, onClose, ...other } = props;
+interface DialogTitleWithCloseButtonProps {
+    onClose: () => void;
+}
 
+const DialogTitleWithCloseButton: React.FC<
+    React.PropsWithChildren<DialogTitleWithCloseButtonProps>
+> = ({ children, onClose }) => {
     return (
-        <DialogTitle {...other}>
+        <DialogTitle>
             <SpaceBetweenFlex>
                 <Typography variant="h3" fontWeight={"bold"}>
                     {children}

+ 1 - 3
web/packages/shared/components/DialogBox/index.tsx

@@ -54,9 +54,7 @@ export default function DialogBox({
             {attributes.title && (
                 <DialogTitleWithCloseButton
                     onClose={
-                        titleCloseButton &&
-                        !attributes.nonClosable &&
-                        handleClose
+                        titleCloseButton && !attributes.nonClosable && onClose
                     }
                 >
                     {attributes.title}

+ 5 - 3
web/packages/shared/components/DialogBoxV2/index.tsx

@@ -15,7 +15,7 @@ import type { DialogBoxAttributesV2 } from "./types";
 type IProps = React.PropsWithChildren<
     Omit<DialogProps, "onClose"> & {
         onClose: () => void;
-        attributes: DialogBoxAttributesV2;
+        attributes?: DialogBoxAttributesV2;
     }
 >;
 
@@ -96,7 +96,9 @@ export default function DialogBoxV2({
                                 size="large"
                                 color={attributes.proceed?.variant}
                                 onClick={async () => {
-                                    await attributes.proceed.action(setLoading);
+                                    await attributes.proceed?.action(
+                                        setLoading,
+                                    );
 
                                     onClose();
                                 }}
@@ -110,7 +112,7 @@ export default function DialogBoxV2({
                                 size="large"
                                 color={attributes.close?.variant ?? "secondary"}
                                 onClick={() => {
-                                    attributes.close.action &&
+                                    attributes.close?.action &&
                                         attributes.close?.action();
                                     onClose();
                                 }}

+ 0 - 4
web/packages/shared/components/DialogBoxV2/types.ts

@@ -39,7 +39,3 @@ export interface DialogBoxAttributesV2 {
     }[];
     buttonDirection?: "row" | "column";
 }
-
-export type SetDialogBoxAttributesV2 = React.Dispatch<
-    React.SetStateAction<DialogBoxAttributesV2>
->;

+ 6 - 11
web/packages/shared/components/EnteButton.tsx

@@ -1,10 +1,6 @@
+import { ensure } from "@/utils/ensure";
 import Done from "@mui/icons-material/Done";
-import {
-    Button,
-    CircularProgress,
-    type ButtonProps,
-    type PaletteColor,
-} from "@mui/material";
+import { Button, CircularProgress, type ButtonProps } from "@mui/material";
 
 interface Iprops extends ButtonProps {
     loading?: boolean;
@@ -26,11 +22,10 @@ export default function EnteButton({
                 ...sx,
                 ...((loading || success) && {
                     "&.Mui-disabled": (theme) => ({
-                        backgroundColor: (
-                            theme.palette[props.color] as PaletteColor
-                        ).main,
-                        color: (theme.palette[props.color] as PaletteColor)
-                            .contrastText,
+                        // TODO: Refactor to not need this ensure.
+                        backgroundColor:
+                            theme.palette[ensure(props.color)].main,
+                        color: theme.palette[ensure(props.color)].contrastText,
                     }),
                 }),
             }}

+ 16 - 6
web/packages/shared/components/EnteLogo.tsx

@@ -1,13 +1,23 @@
 import { styled } from "@mui/material";
+import React from "react";
+
+interface EnteLogoProps {
+    /**
+     * The height of the logo image.
+     *
+     * Default: 18
+     */
+    height?: number;
+}
+
+export const EnteLogo: React.FC<EnteLogoProps> = ({ height }) => {
+    return (
+        <LogoImage height={height ?? 18} alt="logo" src="/images/ente.svg" />
+    );
+};
 
 const LogoImage = styled("img")`
     margin: 3px 0;
     pointer-events: none;
     vertical-align: middle;
 `;
-
-export function EnteLogo(props) {
-    return (
-        <LogoImage height={18} alt="logo" src="/images/ente.svg" {...props} />
-    );
-}

+ 7 - 2
web/packages/shared/components/Navbar/app.tsx

@@ -1,8 +1,13 @@
+import React from "react";
 import { CenteredFlex } from "../../components/Container";
 import { EnteLogo } from "../EnteLogo";
 import NavbarBase from "./base";
 
-export default function AppNavbar({ isMobile }) {
+interface AppNavbarProps {
+    isMobile: boolean;
+}
+
+export const AppNavbar: React.FC<AppNavbarProps> = ({ isMobile }) => {
     return (
         <NavbarBase isMobile={isMobile}>
             <CenteredFlex>
@@ -10,4 +15,4 @@ export default function AppNavbar({ isMobile }) {
             </CenteredFlex>
         </NavbarBase>
     );
-}
+};

+ 1 - 1
web/packages/shared/components/OverflowMenu/context.tsx

@@ -1,5 +1,5 @@
 import { createContext } from "react";
 
 export const OverflowMenuContext = createContext({
-    close: () => null,
+    close: () => {},
 });

+ 4 - 2
web/packages/shared/components/OverflowMenu/menu.tsx

@@ -1,5 +1,5 @@
 import { IconButton, styled, type PaperProps } from "@mui/material";
-import Menu from "@mui/material/Menu";
+import Menu, { type MenuProps } from "@mui/material/Menu";
 import React, { useState } from "react";
 import { OverflowMenuContext } from "./context";
 
@@ -31,7 +31,9 @@ export default function OverflowMenu({
     triggerButtonProps,
     menuPaperProps,
 }: Iprops) {
-    const [sortByEl, setSortByEl] = useState(null);
+    const [sortByEl, setSortByEl] = useState<MenuProps["anchorEl"] | null>(
+        null,
+    );
     const handleClose = () => setSortByEl(null);
     return (
         <OverflowMenuContext.Provider value={{ close: handleClose }}>

+ 3 - 2
web/packages/shared/components/RecoveryKey/index.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import type { PageProps } from "@ente/shared/apps/types";
 import CodeBlock from "@ente/shared/components/CodeBlock";
 import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
@@ -28,7 +29,7 @@ interface Props {
 }
 
 function RecoveryKey({ somethingWentWrong, appContext, ...props }: Props) {
-    const [recoveryKey, setRecoveryKey] = useState(null);
+    const [recoveryKey, setRecoveryKey] = useState<string | null>(null);
 
     useEffect(() => {
         if (!props.show) {
@@ -47,7 +48,7 @@ function RecoveryKey({ somethingWentWrong, appContext, ...props }: Props) {
     }, [props.show]);
 
     function onSaveClick() {
-        downloadAsFile(RECOVERY_KEY_FILE_NAME, recoveryKey);
+        downloadAsFile(RECOVERY_KEY_FILE_NAME, ensure(recoveryKey));
         props.onHide();
     }
 

+ 3 - 1
web/packages/shared/components/ThemeSwitcher.tsx

@@ -2,10 +2,12 @@ import { THEME_COLOR } from "@ente/shared/themes/constants";
 import DarkModeIcon from "@mui/icons-material/DarkMode";
 import LightModeIcon from "@mui/icons-material/LightMode";
 import { ToggleButton, ToggleButtonGroup } from "@mui/material";
+
 interface Iprops {
     themeColor: THEME_COLOR;
-    setThemeColor: (theme: THEME_COLOR) => void;
+    setThemeColor: (themeColor: THEME_COLOR) => void;
 }
+
 export default function ThemeSwitcher({ themeColor, setThemeColor }: Iprops) {
     const handleChange = (event, themeColor: THEME_COLOR) => {
         if (themeColor !== null) {

+ 6 - 5
web/packages/shared/components/VerifyMasterPasswordForm.tsx

@@ -10,8 +10,8 @@ import { CustomError } from "../error";
 import type { KeyAttributes, User } from "../user/types";
 
 export interface VerifyMasterPasswordFormProps {
-    user: User;
-    keyAttributes: KeyAttributes;
+    user: User | undefined;
+    keyAttributes: KeyAttributes | undefined;
     callback: (
         key: string,
         kek: string,
@@ -20,7 +20,7 @@ export interface VerifyMasterPasswordFormProps {
     ) => void;
     buttonText: string;
     submitButtonProps?: ButtonProps;
-    getKeyAttributes?: (kek: string) => Promise<KeyAttributes>;
+    getKeyAttributes?: (kek: string) => Promise<KeyAttributes | undefined>;
     srpAttributes?: SRPAttributes;
 }
 
@@ -48,14 +48,15 @@ export default function VerifyMasterPasswordForm({
                         srpAttributes.opsLimit,
                         srpAttributes.memLimit,
                     );
-                } else {
+                } else if (keyAttributes) {
                     kek = await cryptoWorker.deriveKey(
                         passphrase,
                         keyAttributes.kekSalt,
                         keyAttributes.opsLimit,
                         keyAttributes.memLimit,
                     );
-                }
+                } else
+                    throw new Error("Both SRP and key attributes are missing");
             } catch (e) {
                 log.error("failed to derive key", e);
                 throw Error(CustomError.WEAK_DEVICE);

+ 1 - 1
web/packages/shared/crypto/helpers.ts

@@ -116,7 +116,6 @@ export async function encryptWithRecoveryKey(key: string) {
 }
 
 export const getRecoveryKey = async () => {
-    let recoveryKey: string = null;
     try {
         const cryptoWorker = await ComlinkCryptoWorker.getInstance();
 
@@ -126,6 +125,7 @@ export const getRecoveryKey = async () => {
             recoveryKeyDecryptionNonce,
         } = keyAttributes;
         const masterKey = await getActualKey();
+        let recoveryKey: string;
         if (recoveryKeyEncryptedWithMasterKey) {
             recoveryKey = await cryptoWorker.decryptB64(
                 recoveryKeyEncryptedWithMasterKey,

+ 1 - 0
web/packages/shared/crypto/internal/libsodium.ts

@@ -308,6 +308,7 @@ export async function deriveSensitiveKey(passphrase: string, salt: string) {
             memLimit /= 2;
         }
     }
+    throw new Error("Failed to derive key: Memory limit exceeded");
 }
 
 export async function deriveInteractiveKey(passphrase: string, salt: string) {

+ 2 - 3
web/packages/shared/hooks/useLocalState.tsx

@@ -1,11 +1,10 @@
 import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
-import type { Dispatch, SetStateAction } from "react";
 import { useEffect, useState } from "react";
 
 export function useLocalState<T>(
     key: LS_KEYS,
-    initialValue?: T,
-): [T, Dispatch<SetStateAction<T>>] {
+    initialValue: T,
+): [T, (newValue: T) => void] {
     const [value, setValue] = useState<T>(initialValue);
 
     useEffect(() => {

+ 3 - 17
web/packages/shared/storage/localStorage/index.ts

@@ -29,19 +29,10 @@ export enum LS_KEYS {
     CLIENT_PACKAGE = "clientPackage",
 }
 
-export const setData = (key: LS_KEYS, value: object) => {
-    if (typeof localStorage === "undefined") {
-        return null;
-    }
+export const setData = (key: LS_KEYS, value: object) =>
     localStorage.setItem(key, JSON.stringify(value));
-};
 
-export const removeData = (key: LS_KEYS) => {
-    if (typeof localStorage === "undefined") {
-        return null;
-    }
-    localStorage.removeItem(key);
-};
+export const removeData = (key: LS_KEYS) => localStorage.removeItem(key);
 
 export const getData = (key: LS_KEYS) => {
     try {
@@ -60,9 +51,4 @@ export const getData = (key: LS_KEYS) => {
     }
 };
 
-export const clearData = () => {
-    if (typeof localStorage === "undefined") {
-        return null;
-    }
-    localStorage.clear();
-};
+export const clearData = () => localStorage.clear();

+ 3 - 20
web/packages/shared/storage/sessionStorage/index.ts

@@ -3,31 +3,14 @@ export enum SESSION_KEYS {
     KEY_ENCRYPTION_KEY = "keyEncryptionKey",
 }
 
-export const setKey = (key: SESSION_KEYS, value: object) => {
-    if (typeof sessionStorage === "undefined") {
-        return null;
-    }
+export const setKey = (key: SESSION_KEYS, value: object) =>
     sessionStorage.setItem(key, JSON.stringify(value));
-};
 
 export const getKey = (key: SESSION_KEYS) => {
-    if (typeof sessionStorage === "undefined") {
-        return null;
-    }
     const value = sessionStorage.getItem(key);
     return value && JSON.parse(value);
 };
 
-export const removeKey = (key: SESSION_KEYS) => {
-    if (typeof sessionStorage === "undefined") {
-        return null;
-    }
-    sessionStorage.removeItem(key);
-};
+export const removeKey = (key: SESSION_KEYS) => sessionStorage.removeItem(key);
 
-export const clearKeys = () => {
-    if (typeof sessionStorage === "undefined") {
-        return null;
-    }
-    sessionStorage.clear();
-};
+export const clearKeys = () => sessionStorage.clear();

+ 22 - 13
web/packages/shared/themes/components.ts

@@ -33,7 +33,7 @@ export const getComponents = (
         styleOverrides: {
             root: {
                 ".MuiBackdrop-root": {
-                    backgroundColor: colors.backdrop.faint,
+                    backgroundColor: colors.backdrop?.faint,
                 },
             },
         },
@@ -42,10 +42,10 @@ export const getComponents = (
         styleOverrides: {
             root: {
                 ".MuiBackdrop-root": {
-                    backgroundColor: colors.backdrop.faint,
+                    backgroundColor: colors.backdrop?.faint,
                 },
                 "& .MuiDialog-paper": {
-                    filter: getDropShadowStyle(colors.shadows.float),
+                    filter: getDropShadowStyle(colors.shadows?.float),
                 },
                 "& .MuiDialogTitle-root": {
                     padding: "16px",
@@ -72,14 +72,14 @@ export const getComponents = (
     },
     MuiLink: {
         defaultProps: {
-            color: colors.accent.A500,
+            color: colors.accent?.A500,
             underline: "none",
         },
         styleOverrides: {
             root: {
                 "&:hover": {
                     underline: "always",
-                    color: colors.accent.A500,
+                    color: colors.accent?.A500,
                 },
             },
         },
@@ -95,8 +95,8 @@ export const getComponents = (
                 borderRadius: "4px",
                 textTransform: "none",
                 fontWeight: "bold",
-                fontSize: typography.body.fontSize,
-                lineHeight: typography.body.lineHeight,
+                fontSize: typography.body?.fontSize,
+                lineHeight: typography.body?.lineHeight,
             },
             startIcon: {
                 marginRight: "12px",
@@ -191,8 +191,8 @@ export const getComponents = (
     },
 });
 
-const getDropShadowStyle = (shadows: Shadow[]) => {
-    return shadows
+const getDropShadowStyle = (shadows: Shadow[] | undefined) => {
+    return (shadows ?? [])
         .map(
             (shadow) =>
                 `drop-shadow(${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.color})`,
@@ -200,20 +200,29 @@ const getDropShadowStyle = (shadows: Shadow[]) => {
         .join(" ");
 };
 
-function getIconColor(ownerState, colors: ThemeColorsOptions) {
+interface IconColorableOwnerState {
+    color?: string;
+    disabled?: boolean;
+}
+
+function getIconColor(
+    ownerState: IconColorableOwnerState,
+    colors: ThemeColorsOptions,
+) {
     switch (ownerState.color) {
         case "primary":
             return {
-                color: colors.stroke.base,
+                color: colors.stroke?.base,
             };
         case "secondary":
             return {
-                color: colors.stroke.muted,
+                color: colors.stroke?.muted,
             };
     }
     if (ownerState.disabled) {
         return {
-            color: colors.stroke.faint,
+            color: colors.stroke?.faint,
         };
     }
+    return {};
 }

+ 1 - 0
web/packages/shared/themes/mui-theme.d.ts

@@ -70,6 +70,7 @@ declare module "@mui/material/Button" {
         success: false;
         info: false;
         warning: false;
+        inherit: false;
     }
 }
 declare module "@mui/material/Checkbox" {

+ 23 - 21
web/packages/shared/themes/palette/index.tsx

@@ -1,3 +1,4 @@
+import { ensure } from "@/utils/ensure";
 import type { PaletteOptions, ThemeColorsOptions } from "@mui/material";
 import { THEME_COLOR } from "../constants";
 
@@ -20,38 +21,39 @@ export const getPalletteOptions = (
 ): PaletteOptions => {
     return {
         primary: {
-            main: colors.fill.base,
-            dark: colors.fill.basePressed,
+            // TODO: Refactor this code to not require this ensure
+            main: ensure(colors.fill?.base),
+            dark: colors.fill?.basePressed,
             contrastText:
-                themeColor === "dark" ? colors.black.base : colors.white.base,
+                themeColor === "dark" ? colors.black?.base : colors.white?.base,
         },
         secondary: {
-            main: colors.fill.faint,
-            dark: colors.fill.faintPressed,
-            contrastText: colors.text.base,
+            main: ensure(colors.fill?.faint),
+            dark: colors.fill?.faintPressed,
+            contrastText: colors.text?.base,
         },
         accent: {
-            main: colors.accent.A500,
-            dark: colors.accent.A700,
-            contrastText: colors.white.base,
+            main: ensure(colors.accent?.A500),
+            dark: colors.accent?.A700,
+            contrastText: colors.white?.base,
         },
         critical: {
-            main: colors.danger.A700,
-            dark: colors.danger.A800,
-            contrastText: colors.white.base,
+            main: ensure(colors.danger?.A700),
+            dark: colors.danger?.A800,
+            contrastText: colors.white?.base,
         },
         background: {
-            default: colors.background.base,
-            paper: colors.background.elevated,
+            default: colors.background?.base,
+            paper: colors.background?.elevated,
         },
         text: {
-            primary: colors.text.base,
-            secondary: colors.text.muted,
-            disabled: colors.text.faint,
-            base: colors.text.base,
-            muted: colors.text.muted,
-            faint: colors.text.faint,
+            primary: colors.text?.base,
+            secondary: colors.text?.muted,
+            disabled: colors.text?.faint,
+            base: colors.text?.base,
+            muted: colors.text?.muted,
+            faint: colors.text?.faint,
         },
-        divider: colors.stroke.faint,
+        divider: colors.stroke?.faint,
     };
 };

+ 0 - 3
web/packages/shared/themes/types.ts

@@ -1,3 +0,0 @@
-import { THEME_COLOR } from "./constants";
-
-export type SetTheme = React.Dispatch<React.SetStateAction<THEME_COLOR>>;

+ 2 - 1
web/packages/shared/tsconfig.json

@@ -17,6 +17,7 @@
         "**/*.tsx",
         "**/*.js",
         "themes/mui-theme.d.ts",
-        "../next/log-web.ts"
+        "../next/log-web.ts",
+        "../next/global-electron.d.ts"
     ]
 }

+ 6 - 0
web/packages/shared/utils/index.ts

@@ -26,6 +26,12 @@ export function isPromise<T>(obj: T | Promise<T>): obj is Promise<T> {
 export async function retryAsyncFunction<T>(
     request: (abort?: () => void) => Promise<T>,
     waitTimeBeforeNextTry?: number[],
+    // Need to use @ts-ignore since this same file is currently included with
+    // varying tsconfigs, and the error is only surfaced in the stricter ones of
+    // them.
+    //
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore TSC fails to detect that the exit of the loop is unreachable
 ): Promise<T> {
     if (!waitTimeBeforeNextTry) waitTimeBeforeNextTry = [2000, 5000, 10000];