This commit is contained in:
Manav Rathi
2025-07-02 17:28:52 +05:30
parent d7b6c771e8
commit 2c57a99b5f
6 changed files with 123 additions and 44 deletions

View File

@@ -15,11 +15,9 @@ import { Sidebar } from "components/Sidebar";
import { Upload } from "components/Upload";
import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog";
import {
getAndClearIsFirstLogin,
getAndClearJustSignedUp,
getData,
isFirstLogin,
justSignedUp,
setIsFirstLogin,
setJustSignedUp,
} from "ente-accounts/services/accounts-db";
import { stashRedirect } from "ente-accounts/services/redirect";
import { isSessionInvalid } from "ente-accounts/services/session";
@@ -299,16 +297,27 @@ const Page: React.FC = () => {
// We are logged in and everything looks fine. Proceed with page
// load initialization.
initSettings();
// One time inits.
preloadImage("/images/subscription-card-background");
initSettings();
await initUserDetailsOrTriggerPull();
setupSelectAllKeyBoardShortcutHandler();
// Show the initial state while the rest of the sequence proceeds.
dispatch({ type: "showAll" });
setIsFirstLoad(isFirstLogin());
if (justSignedUp()) {
// If this is the user's first login on this client, then show them
// a message informing the that the initial load might take time.
setIsFirstLoad(getAndClearIsFirstLogin());
// If the user created a new account on this client, show them the
// plan options.
if (getAndClearJustSignedUp()) {
showPlanSelector();
}
setIsFirstLogin(false);
// Initialize the reducer.
const user = getData("user");
// TODO: Pass entire snapshot to reducer?
const familyData = userDetailsSnapshot()?.familyData;
@@ -320,13 +329,19 @@ const Page: React.FC = () => {
collectionFiles: await savedCollectionFiles(),
trashItems: await savedTrashItems(),
});
// Fetch data from remote.
await remotePull();
// Clear the first load message if needed.
setIsFirstLoad(false);
setJustSignedUp(false);
// Start the interval that does a periodic pull.
syncIntervalID = setInterval(
() => remotePull({ silent: true }),
5 * 60 * 1000 /* 5 minutes */,
);
if (electron) {
electron.onMainWindowFocus(() => remotePull({ silent: true }));
if (await shouldShowWhatsNew(electron)) showWhatsNew();

View File

@@ -14,8 +14,8 @@ import {
Typography,
} from "@mui/material";
import {
saveJustSignedUp,
setData,
setJustSignedUp,
setLocalReferralSource,
} from "ente-accounts/services/accounts-db";
import {
@@ -154,7 +154,7 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
);
await saveMasterKeyInSessionAndSafeStore(masterKey);
setJustSignedUp(true);
saveJustSignedUp();
void router.push("/verify");
} catch (e) {
log.error("Signup failed", e);

View File

@@ -17,9 +17,9 @@ import {
import {
getData,
getToken,
isFirstLogin,
savedIsFirstLogin,
saveIsFirstLogin,
setData,
setIsFirstLogin,
setLSUser,
} from "ente-accounts/services/accounts-db";
import {
@@ -98,7 +98,7 @@ const Page: React.FC = () => {
setData("srpAttributes", session.updatedSRPAttributes);
// Set a flag that causes new interactive key attributes to
// be generated.
setIsFirstLogin(true);
saveIsFirstLogin();
// This should be a rare occurrence, instead of building the
// scaffolding to update all the in-memory state, just
// reload everything.
@@ -191,7 +191,7 @@ const Page: React.FC = () => {
await userVerificationResultAfterResolvingSecondFactorChoice(
await verifySRP(srpAttributes!, kek),
);
setIsFirstLogin(true);
saveIsFirstLogin();
if (passkeySessionID) {
await stashKeyEncryptionKeyInSessionStore(kek);
@@ -246,7 +246,7 @@ const Page: React.FC = () => {
const handleVerifyMasterPassword: VerifyMasterPasswordFormProps["onVerify"] =
(key, kek, keyAttributes, password) => {
void (async () => {
const updatedKeyAttributes = isFirstLogin()
const updatedKeyAttributes = savedIsFirstLogin()
? await generateAndSaveInteractiveKeyAttributes(
password,
keyAttributes,

View File

@@ -7,8 +7,8 @@ import {
import { RecoveryKey } from "ente-accounts/components/RecoveryKey";
import {
getData,
justSignedUp,
setJustSignedUp,
savedJustSignedUp,
saveJustSignedUp,
} from "ente-accounts/services/accounts-db";
import { appHomeRoute } from "ente-accounts/services/redirect";
import {
@@ -54,7 +54,7 @@ const Page: React.FC = () => {
if (!user?.token) {
void router.push("/");
} else if (haveCredentialsInSession()) {
if (justSignedUp()) {
if (savedJustSignedUp()) {
setOpenRecoveryKey(true);
setLoading(false);
} else {
@@ -82,7 +82,7 @@ const Page: React.FC = () => {
masterKey,
);
await saveMasterKeyInSessionAndSafeStore(masterKey);
setJustSignedUp(true);
saveJustSignedUp();
setOpenRecoveryKey(true);
} catch (e) {
log.error("failed to generate password", e);

View File

@@ -10,8 +10,8 @@ import { useSecondFactorChoiceIfNeeded } from "ente-accounts/components/utils/se
import {
getData,
getLocalReferralSource,
saveIsFirstLogin,
setData,
setIsFirstLogin,
setLSUser,
} from "ente-accounts/services/accounts-db";
import {
@@ -107,12 +107,7 @@ const Page: React.FC = () => {
isTwoFactorEnabled: true,
isTwoFactorPasskeysEnabled: true,
});
// TODO: This is not the first login though if they already have
// 2FA. Does this flag mean first login on this device?
//
// Update: This flag causes the interactive encryption key to be
// generated, so it has a functional impact we need.
setIsFirstLogin(true);
saveIsFirstLogin();
const url = passkeyVerificationRedirectURL(
accountsUrl!,
passkeySessionID,
@@ -125,7 +120,7 @@ const Page: React.FC = () => {
twoFactorSessionID,
isTwoFactorEnabled: true,
});
setIsFirstLogin(true);
saveIsFirstLogin();
void router.push("/two-factor/verify");
} else {
await setLSUser({
@@ -147,7 +142,7 @@ const Page: React.FC = () => {
}
await unstashAndUseSRPSetupAttributes(setupSRP);
}
setIsFirstLogin(true);
saveIsFirstLogin();
const redirectURL = unstashRedirect();
if (keyAttributes?.encryptedKey) {
clearSessionStorage();

View File

@@ -1,7 +1,8 @@
import { getKVS, removeKV, setKV } from "ente-base/kv";
import log from "ente-base/log";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
import { RemoteKeyAttributes, type KeyAttributes } from "./user";
export type LocalStorageKey =
| "user"
// See also savedKeyAttributes.
@@ -15,12 +16,6 @@ export type LocalStorageKey =
| "srpAttributes"
| "referralSource";
export const setData = (key: LocalStorageKey, value: object) =>
localStorage.setItem(key, JSON.stringify(value));
export const removeData = (key: LocalStorageKey) =>
localStorage.removeItem(key);
/**
* [Note: Accounts DB]
*
@@ -55,6 +50,9 @@ export const getData = (key: LocalStorageKey) => {
}
};
export const setData = (key: LocalStorageKey, value: object) =>
localStorage.setItem(key, JSON.stringify(value));
// TODO: Migrate this to `local-user.ts`, with (a) more precise optionality
// indication of the constituent fields, (b) moving any fields that need to be
// accessed from web workers to KV DB.
@@ -153,17 +151,88 @@ export const getToken = (): string => {
return token;
};
export const isFirstLogin = () => getData("isFirstLogin")?.status ?? false;
const LocalIsFirstLogin = z.object({
status: z.boolean().nullish().transform(nullToUndefined),
});
export function setIsFirstLogin(status: boolean) {
setData("isFirstLogin", { status });
}
/**
* Return `true` if it is the user's first login on this client.
*
* The {@link savedIsFirstLogin} flag is saved in local storage (using
* {@link saveIsFirstLogin}) if we determine during the login flow that it a
* fresh login on this client. If so, we
*
* - Generate interactive key attributes for them, and
*
* - Display them a special indicator post login to notify them that the first
* load might take extra time. At this point, we also clear the flag (the read
* and clear is done by the same {@link getAndClearIsFirstLogin} function).
*/
export const savedIsFirstLogin = () => {
const jsonString = localStorage.getItem("isFirstLogin");
if (!jsonString) return false;
return LocalIsFirstLogin.parse(JSON.parse(jsonString)).status ?? false;
};
export const justSignedUp = () => getData("justSignedUp")?.status ?? false;
/**
* Save a flag in local storage to indicate that this is the user's first login
* on this client.
*
* This is the setter corresponding to {@link savedIsFirstLogin}.
*/
export const saveIsFirstLogin = () => {
localStorage.setItem("isFirstLogin", JSON.stringify({ status: true }));
};
export function setJustSignedUp(status: boolean) {
setData("justSignedUp", { status });
}
/**
* Get the saved value of the local storage flag that indicates that this is the
* user' first login on this client. Also remove the flag after reading.
*
* The flag can be set by using {@link saveIsFirstLogin}, and can be read
* without clearing it by using {@link savedIsFirstLogin}.
*/
export const getAndClearIsFirstLogin = () => {
const result = savedIsFirstLogin();
localStorage.removeItem("isFirstLogin");
return result;
};
const LocalJustSignedUp = z.object({
status: z.boolean().nullish().transform(nullToUndefined),
});
/**
* Return `true` if the user created a new account on this client during the
* current (in-progress) or just completed login / signup sequence.
*/
export const savedJustSignedUp = () => {
const jsonString = localStorage.getItem("justSignedUp");
if (!jsonString) return false;
return LocalJustSignedUp.parse(JSON.parse(jsonString)).status ?? false;
};
/**
* Save a flag in local storage to indicate that the user signed up for a
* new Ente account during the current login / signup sequence.
*
* This is the setter corresponding to {@link savedJustSignedUp}.
*/
export const saveJustSignedUp = () => {
localStorage.setItem("justSignedUp", JSON.stringify({ status: true }));
};
/**
* Get the saved value of the local storage flag that indicates that the user just
* signed up. Also remove the flag from local storage after reading.
*
* The flag can be set by using {@link saveJustSignedUp}, and can be read
* without clearing it by using {@link savedJustSignedUp}.
*/
export const getAndClearJustSignedUp = () => {
const result = savedJustSignedUp();
localStorage.removeItem("justSignedUp");
return result;
};
export function getLocalReferralSource() {
return getData("referralSource")?.source;