Skip to content

Commit

Permalink
Fix review
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Sep 4, 2024
1 parent af66477 commit 47259df
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@ import {
type UmamiStore,
generate24WordMnemonic,
makeStore,
useGetDecryptedMnemonic,
useIsPasswordSet,
useRestoreFromMnemonic,
useRestoreFromSecretKey,
} from "@umami/state";
import { mnemonic1 } from "@umami/test-utils";

import { SetupPassword } from "./SetupPassword";
import { act, fireEvent, renderInModal, screen, userEvent, waitFor } from "../../../testUtils";
import {
act,
dynamicModalContextMock,
fireEvent,
renderInModal,
screen,
userEvent,
waitFor,
} from "../../../testUtils";
import { CURVES } from "../AdvancedAccountSettings";
import { ImportantNoticeModal } from "../VerificationFlow/ImportantNoticeModal";

jest.mock("@umami/state", () => ({
...jest.requireActual("@umami/state"),
useRestoreFromMnemonic: jest.fn(),
useRestoreFromSecretKey: jest.fn(),
generate24WordMnemonic: jest.fn(),
useGetDecryptedMnemonic: jest.fn(),
useIsPasswordSet: jest.fn(),
}));

const password = "password";

beforeEach(() => {
(useGetDecryptedMnemonic as jest.Mock).mockImplementation(() => () => mnemonic1);
});

describe("<SetupPassword />", () => {
describe.each(["mnemonic", "secret_key"] as const)("%s mode", mode => {
describe("validations", () => {
Expand Down Expand Up @@ -250,4 +267,26 @@ describe("<SetupPassword />", () => {
});
});
});

describe("verification mode", () => {
it("renders the verification screen", async () => {
jest.mocked(useIsPasswordSet).mockReturnValue(true);
const { openWith } = dynamicModalContextMock;
const user = userEvent.setup();

await renderInModal(<SetupPassword mode="verification" />);

const passwordInput = screen.getByLabelText("Password");

await act(() => user.type(passwordInput, password));

const submitButton = screen.getByRole("button", { name: "Confirm" });

await act(() => user.click(submitButton));

expect(openWith).toHaveBeenCalledWith(<ImportantNoticeModal seedPhrase={mnemonic1} />, {
size: "xl",
});
});
});
});
87 changes: 52 additions & 35 deletions apps/web/src/components/Onboarding/SetupPassword/SetupPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
} from "@chakra-ui/react";
import { type Curves } from "@taquito/signer";
import { useDynamicModalContext, useMultiForm } from "@umami/components";
import { DEFAULT_ACCOUNT_LABEL } from "@umami/core";
import { DEFAULT_ACCOUNT_LABEL, type MnemonicAccount } from "@umami/core";
import {
accountsActions,
generate24WordMnemonic,
useAppDispatch,
useAsyncActionHandler,
useCurrentAccount,
useGetDecryptedMnemonic,
useGetNextAvailableAccountLabels,
useIsPasswordSet,
useRestoreFromMnemonic,
Expand All @@ -33,6 +35,7 @@ import { ModalBackButton } from "../../BackButton";
import { ModalCloseButton } from "../../CloseButton";
import { PasswordInput } from "../../PasswordInput";
import { AdvancedAccountSettings } from "../AdvancedAccountSettings";
import { ImportantNoticeModal } from "../VerificationFlow/ImportantNoticeModal";

type FormFields = {
password: string;
Expand All @@ -41,26 +44,51 @@ type FormFields = {
curve: Exclude<Curves, "bip25519">;
};

type Mode = "mnemonic" | "secret_key" | "new_mnemonic";
type Mode = "mnemonic" | "secret_key" | "new_mnemonic" | "verification";

type SetupPasswordProps = {
mode?: Mode;
handleProceedToVerification?: (password: string) => void;
mode: Mode;
};

export const SetupPassword = ({
mode,
handleProceedToVerification: handleProceedToVerification,
}: SetupPasswordProps) => {
const getModeConfig = (mode: Mode) => {
switch (mode) {
case "verification":
return {
icon: LockIcon,
title: "Confirm password",
buttonLabel: "Confirm",
subtitle: "Confirm the password to secure your wallet and verification process.",
};
case "new_mnemonic":
return {
icon: UserIcon,
title: "Create Password",
buttonLabel: "Create Account",
subtitle: "Set a password to unlock your wallet. Make sure to store your password safely.",
};
case "mnemonic":
case "secret_key":
return {
icon: LockIcon,
title: "Almost there",
buttonLabel: "Import Wallet",
};
}
};

export const SetupPassword = ({ mode }: SetupPasswordProps) => {
const color = useColor();
const { handleAsyncAction, isLoading } = useAsyncActionHandler();
const { allFormValues, onClose } = useDynamicModalContext();
const dispatch = useAppDispatch();

const { allFormValues, onClose, openWith } = useDynamicModalContext();
const restoreFromMnemonic = useRestoreFromMnemonic();
const restoreFromSecretKey = useRestoreFromSecretKey();
const checkPassword = useValidateMasterPassword();
const getNextAvailableAccountLabels = useGetNextAvailableAccountLabels();
const isPasswordSet = useIsPasswordSet();
const dispatch = useAppDispatch();
const getDecryptedMnemonic = useGetDecryptedMnemonic();
const currentAccount = useCurrentAccount() as MnemonicAccount;

const form = useMultiForm<FormFields>({
mode: "onBlur",
Expand All @@ -77,18 +105,6 @@ export const SetupPassword = ({

const isNewMnemonic = mode === "new_mnemonic";

const onHandleSubmit = (formFields: FormFields) => {
if (handleProceedToVerification) {
return handleProceedToVerification(formFields.password);
}

if (isNewMnemonic) {
dispatch(accountsActions.setPassword(formFields.password));
}

return onSubmit(formFields);
};

const onSubmit = ({ password, curve, derivationPath }: FormFields) =>
handleAsyncAction(async () => {
const label = getNextAvailableAccountLabels(DEFAULT_ACCOUNT_LABEL)[0];
Expand Down Expand Up @@ -118,18 +134,24 @@ export const SetupPassword = ({
curve,
isVerified: !isNewMnemonic,
});

if (isNewMnemonic) {
dispatch(accountsActions.setPassword(password));
}

break;
}
default:
break;
case "verification": {
const mnemonic = await getDecryptedMnemonic(currentAccount, password);

return openWith(<ImportantNoticeModal seedPhrase={mnemonic} />, { size: "xl" });
}
}

return onClose();
});

const icon = isNewMnemonic ? UserIcon : LockIcon;
const title = mode ? (isNewMnemonic ? "Create Password" : "Almost there") : "Confirm password";
const buttonLabel = mode ? (isNewMnemonic ? "Create Account" : "Import Wallet") : "Confirm";
const { icon, title, buttonLabel, subtitle } = getModeConfig(mode);

return (
<ModalContent>
Expand All @@ -139,20 +161,15 @@ export const SetupPassword = ({
<Center flexDirection="column" gap="16px">
<Icon as={icon} width="24px" height="24px" color={color("blue")} />
<Heading size="xl">{title}</Heading>
{isNewMnemonic && (
<Text width="full" color={color("700")} fontWeight="400" textAlign="center" size="md">
Set a password to unlock your wallet. Make sure to store your password safely.
</Text>
)}
{!mode && (
{subtitle && (
<Text width="full" color={color("700")} fontWeight="400" textAlign="center" size="md">
Confirm the password to secure your wallet and verification process.
{subtitle}
</Text>
)}
</Center>
</ModalHeader>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onHandleSubmit)}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<ModalBody>
<Flex flexDirection="column" gap="24px">
<PasswordInput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ export const ImportantNoticeModal = ({ seedPhrase }: ImportantNoticeModalProps)
<ModalFooter>
<Button
width="full"
onClick={() => openWith(<RecordSeedphraseModal seedPhrase={seedPhrase} />)}
onClick={() =>
openWith(<RecordSeedphraseModal seedPhrase={seedPhrase} />, { size: "xl" })
}
variant="primary"
>
Next
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const RecordSeedphraseModal = ({ seedPhrase }: CopySeedphraseModalProps)
<ModalCloseButton />
<Center flexDirection="column" gap="12px">
<Icon as={KeyIcon} boxSize="24px" marginBottom="4px" color={color("blue")} />
<Heading size="xl">Import Wallet</Heading>
<Heading size="xl">Record Seed Phrase</Heading>
<Text width="full" color={color("700")} fontWeight="400" textAlign="center" size="md">
Record these 24 words in order to restore your wallet in the future
</Text>
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/components/Onboarding/VerificationFlow/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
export { RecordSeedphraseModal } from "./RecordSeedphraseModal";
export { VerificationInfoModal } from "./VerificationInfoModal";
export { VerifyMessage } from "./VerifyMessage";
export { useIsAccountVerified } from "./useIsAccountVerified";
export { ImportantNoticeModal } from "./ImportantNoticeModal";
export { VerifySeedphraseModal } from "./VerifySeedphraseModal";
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,19 @@ beforeEach(() => {
describe("useHandleVerify", () => {
it("should open SetupPassword modal if master password is not set", async () => {
const { openWith } = dynamicModalContextMock;
const { result } = renderHook(() => useHandleVerify(), { store });
const { result } = renderHook(useHandleVerify, { store });

await act(async () => await result.current());

expect(openWith).toHaveBeenCalledWith(
<SetupPassword handleProceedToVerification={expect.any(Function)} />
);
expect(openWith).toHaveBeenCalledWith(<SetupPassword mode="verification" />);
});

it("should open ImportantNoticeModal modal if master password is set", async () => {
(useGetDecryptedMnemonic as jest.Mock).mockImplementation(() => () => mnemonic1);
store.dispatch(accountsActions.setPassword("password"));

const { openWith } = dynamicModalContextMock;
const { result } = renderHook(() => useHandleVerify(), { store });
const { result } = renderHook(useHandleVerify, { store });

await act(() => result.current());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,16 @@ export const useHandleVerify = () => {
const currentAccount = useCurrentAccount() as MnemonicAccount;
const getDecryptedMnemonic = useGetDecryptedMnemonic();
const { handleAsyncAction } = useAsyncActionHandler();
const { password: masterPassword } = useAppSelector(state => state.accounts);
const { password } = useAppSelector(state => state.accounts);

const handleProceedToVerification = async (password: string) =>
return () =>
handleAsyncAction(async () => {
const mnemonic = await getDecryptedMnemonic(currentAccount, password);
if (password) {
const mnemonic = await getDecryptedMnemonic(currentAccount, password);

return openWith(<ImportantNoticeModal seedPhrase={mnemonic} />, { size: "xl" });
});

return () => {
if (masterPassword) {
return handleProceedToVerification(masterPassword);
}
return openWith(<ImportantNoticeModal seedPhrase={mnemonic} />, { size: "xl" });
}

return openWith(<SetupPassword handleProceedToVerification={handleProceedToVerification} />);
};
return openWith(<SetupPassword mode="verification" />);
});
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mockMnemonicAccount, mockSocialAccount } from "@umami/core";
import { mockLedgerAccount, mockMnemonicAccount, mockSocialAccount } from "@umami/core";
import { type UmamiStore, addTestAccount, makeStore } from "@umami/state";

import { useIsAccountVerified } from "./useIsAccountVerified";
Expand All @@ -11,24 +11,16 @@ beforeEach(() => {
});

describe("useIsAccountVerified", () => {
it("returns true if the account is not mnemonic", () => {
addTestAccount(store, mockSocialAccount(0));
it.each([
// verified status, account type, account
[true, "social", mockSocialAccount(0)],
[true, "ledger", mockLedgerAccount(0)],
[true, "verified mnemonic", mockMnemonicAccount(0)],
[false, "unverified mnemonic", mockMnemonicAccount(0, { isVerified: false })],
])("returns %s for %s account", (isVerified, _, account) => {
addTestAccount(store, account);
const { result } = renderHook(() => useIsAccountVerified(), { store });

expect(result.current).toBe(true);
});

it("returns true if the account is mnemonic and verified", () => {
addTestAccount(store, mockMnemonicAccount(0));
const { result } = renderHook(() => useIsAccountVerified(), { store });

expect(result.current).toBe(true);
});

it("returns false if the account is mnemonic and not verified", () => {
addTestAccount(store, mockMnemonicAccount(0, { isVerified: false }));
const { result } = renderHook(() => useIsAccountVerified(), { store });

expect(result.current).toBe(false);
expect(result.current).toBe(isVerified);
});
});
12 changes: 3 additions & 9 deletions packages/state/src/hooks/getAccountData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ export const useGetMostFundedImplicitAccount = () => {
};

export const useGetSecretKey = () => {
const seedPhrases = useSeedPhrases();
const encryptedSecretKeys = useSecretKeys();
const getDecryptedMnemonic = useGetDecryptedMnemonic();

return async (account: MnemonicAccount | SecretKeyAccount, password: string) => {
if (account.type === "secret_key") {
Expand All @@ -178,12 +178,7 @@ export const useGetSecretKey = () => {
return decrypt(encryptedSecretKey, password);
}

const encryptedMnemonic = seedPhrases[account.seedFingerPrint];
if (!encryptedMnemonic) {
throw new Error(`Missing seedphrase for account ${account.address.pkh}`);
}

const mnemonic = await decrypt(encryptedMnemonic, password);
const mnemonic = await getDecryptedMnemonic(account, password);
return deriveSecretKey(mnemonic, account.derivationPath, account.curve);
};
};
Expand Down Expand Up @@ -216,7 +211,6 @@ export const useGetDecryptedMnemonic = () => {
throw new Error(`Missing seedphrase for account ${account.address.pkh}`);
}

const mnemonic = await decrypt(encryptedMnemonic, password);
return mnemonic;
return decrypt(encryptedMnemonic, password);
};
};

0 comments on commit 47259df

Please sign in to comment.