Skip to content

Commit

Permalink
feat: new ui for mnemonic onboarding (#2991)
Browse files Browse the repository at this point in the history
* feat: new ui for mnemonic onboarding

* feat: new mnemonic screen ui

* chore: code improvement

* feat: new design for generate masterkey and import master key page

* feat: shift backup confirmation to the mnemonic inputs

* chore: consistent input field width

* feat: change password adornment icon

* fix: optimize svg
fix translations

* chore: icon optmizations

* fix: code quality checks

* fix: replace try / catch

* fix: revert images

* fix: updated images

* fix: update flex layout

* fix: cleanup icons

* feat: set backup parameter from backup screen

* fix: hover color

* fix: add backup check to account settings

* fix: strict check

* fix: removed backup success message

* fix: reset translations

---------

Co-authored-by: René Aaron <[email protected]>
Co-authored-by: René Aaron <[email protected]>
  • Loading branch information
3 people authored Feb 27, 2024
1 parent 1acbd97 commit d8efb71
Show file tree
Hide file tree
Showing 29 changed files with 261 additions and 150 deletions.
2 changes: 1 addition & 1 deletion src/app/components/ContentBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

export function ContentBox({ children }: React.PropsWithChildren<object>) {
return (
<div className="mt-6 bg-white rounded-md p-8 border border-gray-200 dark:border-neutral-700 dark:bg-surface-01dp flex flex-col gap-6">
<div className="mt-6 bg-white rounded-2xl p-10 border border-gray-200 dark:border-neutral-700 dark:bg-surface-01dp flex flex-col gap-6">
{children}
</div>
);
Expand Down
12 changes: 5 additions & 7 deletions src/app/components/PasswordViewAdornment/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
HiddenIcon,
VisibleIcon,
} from "@bitcoin-design/bitcoin-icons-react/outline";
import { PopiconsEyeLine } from "@popicons/react";
import { useEffect, useState } from "react";
import EyeCrossedIcon from "~/app/icons/EyeCrossedIcon";

type Props = {
onChange: (viewingPassword: boolean) => void;
Expand All @@ -23,16 +21,16 @@ export default function PasswordViewAdornment({ onChange, isRevealed }: Props) {
<button
type="button"
tabIndex={-1}
className="flex justify-center items-center w-10 h-8"
className="flex justify-center items-center w-10 h-8 text-gray-400 dark:text-neutral-600 hover:text-gray-600 hover:dark:text-neutral-400"
onClick={() => {
setRevealed(!_isRevealed);
onChange(!_isRevealed);
}}
>
{_isRevealed ? (
<VisibleIcon className="h-6 w-6" />
<EyeCrossedIcon className="h-6 w-6" />
) : (
<HiddenIcon className="h-6 w-6" />
<PopiconsEyeLine className="h-6 w-6" />
)}
</button>
);
Expand Down
58 changes: 58 additions & 0 deletions src/app/components/mnemonic/MnemonicBackupDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
PopiconsLifebuoyLine,
PopiconsShieldLine,
PopiconsTriangleExclamationLine,
} from "@popicons/react";
import { useTranslation } from "react-i18next";
import { classNames } from "~/app/utils";

function MnemonicInstructions() {
const { t } = useTranslation("translation", {
keyPrefix: "accounts.account_view.mnemonic",
});

return (
<>
<div className="flex flex-col gap-4">
<ListItem
icon={<PopiconsLifebuoyLine />}
title={t("description.recovery_phrase")}
type="info"
/>
<ListItem
icon={<PopiconsShieldLine />}
title={t("description.secure_recovery_phrase")}
type="info"
/>
<ListItem
icon={<PopiconsTriangleExclamationLine />}
title={t("description.warning")}
type="warn"
/>
</div>
</>
);
}

export default MnemonicInstructions;

type ListItemProps = {
icon: React.ReactNode;
title: string;
type: "warn" | "info";
};

function ListItem({ icon, title, type }: ListItemProps) {
return (
<div
className={classNames(
type == "warn" && "text-orange-700 dark:text-orange-200",
type == "info" && "text-gray-600 dark:text-neutral-400",
"flex gap-2 items-center text-sm"
)}
>
<div className="shrink-0">{icon}</div>
<span>{title}</span>
</div>
);
}
27 changes: 10 additions & 17 deletions src/app/components/mnemonic/MnemonicDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
KeyIcon,
MnemonicIcon,
PasswordIcon,
SafeIcon,
} from "@bitcoin-design/bitcoin-icons-react/outline";
import { PopiconsDownloadLine, PopiconsKeyLine } from "@popicons/react";
import { useTranslation } from "react-i18next";
import FaceSurpriseIcon from "~/app/icons/FaceSurpriseIcon";

function MnemonicDescription() {
const { t } = useTranslation("translation", {
Expand All @@ -13,14 +9,13 @@ function MnemonicDescription() {

return (
<>
<div className="flex flex-col gap-2">
<ListItem icon={<KeyIcon />} title={t("backup.items.keys")} />
<div className="flex flex-col gap-4">
<ListItem icon={<PopiconsKeyLine />} title={t("new.items.keys")} />
<ListItem icon={<FaceSurpriseIcon />} title={t("new.items.usage")} />
<ListItem
icon={<MnemonicIcon />}
title={t("backup.items.recovery_phrase")}
icon={<PopiconsDownloadLine />}
title={t("new.items.recovery_phrase")}
/>
<ListItem icon={<PasswordIcon />} title={t("backup.items.words")} />
<ListItem icon={<SafeIcon />} title={t("backup.items.storage")} />
</div>
</>
);
Expand All @@ -32,11 +27,9 @@ type ListItemProps = { icon: React.ReactNode; title: string };

function ListItem({ icon, title }: ListItemProps) {
return (
<div className="flex gap-2 items-center">
<div className="shrink-0 w-8 h-8 text-gray-600 dark:text-neutral-400">
{icon}
</div>
<span className="text-gray-600 dark:text-neutral-400">{title}</span>
<div className="flex gap-2 items-center text-gray-600 dark:text-neutral-400 text-sm">
<div className="shrink-0">{icon}</div>
<span>{title}</span>
</div>
);
}
32 changes: 28 additions & 4 deletions src/app/components/mnemonic/MnemonicInputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@ import { wordlist } from "@scure/bip39/wordlists/english";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import PasswordViewAdornment from "~/app/components/PasswordViewAdornment";
import Checkbox from "~/app/components/form/Checkbox";
import Input from "~/app/components/form/Input";

type MnemonicInputsProps = {
mnemonic?: string;
setMnemonic?(mnemonic: string): void;
readOnly?: boolean;
isConfirmed?: (confirmed: boolean) => void;
};

export default function MnemonicInputs({
mnemonic,
setMnemonic,
readOnly,
children,
isConfirmed,
}: React.PropsWithChildren<MnemonicInputsProps>) {
const { t } = useTranslation("translation", {
keyPrefix: "accounts.account_view.mnemonic",
});
const [revealedIndex, setRevealedIndex] = useState<number | undefined>(
undefined
);
const [hasConfirmedBackup, setHasConfirmedBackup] = useState(false);

const words = mnemonic?.split(" ") || [];
while (words.length < 12) {
Expand All @@ -32,7 +36,7 @@ export default function MnemonicInputs({
}

return (
<div className="border border-gray-200 dark:border-neutral-700 rounded-lg p-6 flex flex-col gap-4 items-center justify-center">
<div className="border border-gray-200 dark:border-neutral-700 rounded-2xl p-6 flex flex-col gap-4 items-center justify-center">
<h3 className="text-lg font-semibold dark:text-white">
{t("inputs.title")}
</h3>
Expand All @@ -42,18 +46,18 @@ export default function MnemonicInputs({
const inputId = `mnemonic-word-${i}`;
return (
<div key={i} className="flex justify-center items-center gap-2">
<span className="text-gray-600 dark:text-neutral-400 text-right">
<span className="w-6 text-gray-600 dark:text-neutral-400">
{i + 1}.
</span>
<div className="relative">
<div className="w-full">
<Input
id={inputId}
autoFocus={!readOnly && i === 0}
onFocus={() => setRevealedIndex(i)}
onBlur={() => setRevealedIndex(undefined)}
readOnly={readOnly}
block={false}
className="w-32 text-center"
className="w-full text-center"
list={readOnly ? undefined : "wordlist"}
value={isRevealed ? word : word.length ? "•••••" : ""}
onChange={(e) => {
Expand Down Expand Up @@ -92,6 +96,26 @@ export default function MnemonicInputs({
</datalist>
)}
{children}

{isConfirmed && (
<div className="flex items-center justify-center mt-4">
<Checkbox
id="has_backed_up"
name="Backup confirmation checkbox"
checked={hasConfirmedBackup}
onChange={(event) => {
setHasConfirmedBackup(event.target.checked);
if (isConfirmed) isConfirmed(event.target.checked);
}}
/>
<label
htmlFor="has_backed_up"
className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white"
>
{t("confirm")}
</label>
</div>
)}
</div>
);
}
27 changes: 27 additions & 0 deletions src/app/icons/EyeCrossedIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SVGProps } from "react";

const EyeCrossedIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={20}
height={20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10 6.21314C8.8144 6.21314 7.83961 6.61698 7.16667 7.34639C6.50383 8.06485 6.20655 9.01986 6.20655 10C6.20655 10.9801 6.50383 11.9352 7.16667 12.6536C7.83961 13.383 8.8144 13.7869 10 13.7869C11.1856 13.7869 12.1604 13.383 12.8333 12.6536C13.4962 11.9352 13.7934 10.9801 13.7934 10C13.7934 9.01986 13.4962 8.06485 12.8333 7.34639C12.1604 6.61698 11.1856 6.21314 10 6.21314ZM8.36461 8.44777C8.68478 8.10074 9.19961 7.83891 10 7.83891C10.8004 7.83891 11.3152 8.10074 11.6354 8.44777C11.9657 8.80575 12.1648 9.33776 12.1648 10C12.1648 10.6622 11.9657 11.1943 11.6354 11.5522C11.3152 11.8993 10.8004 12.1611 10 12.1611C9.19961 12.1611 8.68478 11.8993 8.36461 11.5522C8.03435 11.1943 7.83515 10.6622 7.83515 10C7.83515 9.33776 8.03435 8.80575 8.36461 8.44777Z"
fill="currentColor"
/>
<path
d="M10 2C6.11297 2 3.58698 3.31971 2.04049 5.02671C0.5183 6.7069 0 8.70232 0 10C0 11.2977 0.5183 13.2931 2.04049 14.9733C3.58698 16.6803 6.11297 18 10 18C13.887 18 16.413 16.6803 17.9595 14.9733C19.4817 13.2931 20 11.2977 20 10C20 8.70232 19.4817 6.7069 17.9595 5.02671C16.413 3.31971 13.887 2 10 2ZM3.17925 6.02303C4.40408 4.67107 6.49692 3.49832 10 3.49832C13.5031 3.49832 15.5959 4.67107 16.8208 6.02303C18.0699 7.4018 18.4753 9.03181 18.4753 10C18.4753 10.9682 18.0699 12.5982 16.8208 13.977C15.5959 15.3289 13.5031 16.5017 10 16.5017C6.49692 16.5017 4.40408 15.3289 3.17925 13.977C1.93013 12.5982 1.52468 10.9682 1.52468 10C1.52468 9.03181 1.93013 7.4018 3.17925 6.02303Z"
fill="currentColor"
/>
<path
d="M18.0474 18.291C18.2636 18.0756 18.3077 17.7531 18.1796 17.4939L3.78294 2.99892L2.99024 2.20361C2.72032 1.93281 2.28193 1.93203 2.01105 2.20187C1.74017 2.4717 1.73938 2.90998 2.0093 3.18078L17.0682 18.2892C17.3381 18.56 17.7765 18.5608 18.0474 18.291Z"
fill="currentColor"
/>
</svg>
);

export default EyeCrossedIcon;
32 changes: 32 additions & 0 deletions src/app/icons/FaceSurpriseIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SVGProps } from "react";

const FaceSurpriseIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={20}
height={20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 5.02944 14.9706 1 10 1ZM2.51285 10C2.51285 5.86496 5.86496 2.51285 10 2.51285C14.135 2.51285 17.4872 5.86496 17.4872 10C17.4872 14.135 14.135 17.4872 10 17.4872C5.86496 17.4872 2.51285 14.135 2.51285 10Z"
fill="currentColor"
/>
<path
d="M12 13C12 14.1046 11.1046 15 10 15C8.89543 15 8 14.1046 8 13C8 11.8954 8.89543 11 10 11C11.1046 11 12 11.8954 12 13Z"
fill="currentColor"
/>
<path
d="M8.46333 8.38462C8.46333 8.9379 8.01481 9.38643 7.46152 9.38643C6.90824 9.38643 6.45972 8.9379 6.45972 8.38462C6.45972 7.83134 6.90824 7.38281 7.46152 7.38281C8.01481 7.38281 8.46333 7.83134 8.46333 8.38462Z"
fill="currentColor"
/>
<path
d="M13.5391 8.38462C13.5391 8.93722 13.0911 9.38519 12.5385 9.38519C11.9859 9.38519 11.5379 8.93722 11.5379 8.38462C11.5379 7.83202 11.9859 7.38405 12.5385 7.38405C13.0911 7.38405 13.5391 7.83202 13.5391 8.38462Z"
fill="currentColor"
/>
</svg>
);
export default FaceSurpriseIcon;
44 changes: 33 additions & 11 deletions src/app/screens/Accounts/BackupMnemonic/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useNavigate, useParams } from "react-router-dom";
import Button from "~/app/components/Button";
import { ContentBox } from "~/app/components/ContentBox";
import toast from "~/app/components/Toast";
import MnemonicDescription from "~/app/components/mnemonic/MnemonicDescription";
import MnemonicInstructions from "~/app/components/mnemonic/MnemonicBackupDescription";
import MnemonicInputs from "~/app/components/mnemonic/MnemonicInputs";
import api from "~/common/lib/api";

Expand All @@ -19,8 +19,9 @@ function BackupMnemonic() {

const [mnemonic, setMnemonic] = useState<string | undefined>();
const [loading, setLoading] = useState<boolean>(true);
const [hasConfirmedBackup, setHasConfirmedBackup] = useState(false);

const { id } = useParams();
const { id } = useParams() as { id: string };

const fetchData = useCallback(async () => {
try {
Expand All @@ -38,7 +39,19 @@ function BackupMnemonic() {
fetchData();
}, [fetchData]);

// TODO: set isMnemonicBackupDone, once new ui for screen is merged
async function completeBackupProcess() {
if (!hasConfirmedBackup) {
toast.error(t("error_confirm"));
return false;
}

await api.editAccount(id, {
isMnemonicBackupDone: true,
});

navigate(`/accounts/${id}`);
}

return loading ? (
<div className="flex justify-center mt-5">
<Loading />
Expand All @@ -50,15 +63,24 @@ function BackupMnemonic() {
<h1 className="font-bold text-2xl dark:text-white">
{t("backup.title")}
</h1>
<MnemonicDescription />
<MnemonicInputs mnemonic={mnemonic} readOnly />
</ContentBox>
<div className="flex justify-center my-6 gap-4">
<Button
label={tCommon("actions.back")}
onClick={() => navigate(-1)}
<MnemonicInstructions />
<MnemonicInputs
mnemonic={mnemonic}
readOnly
isConfirmed={(hasConfirmedBackup) => {
setHasConfirmedBackup(hasConfirmedBackup);
}}
/>
</div>

<div className="flex justify-center mt-6 w-64 mx-auto">
<Button
label={tCommon("actions.finish")}
primary
flex
onClick={completeBackupProcess}
/>
</div>
</ContentBox>
</Container>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion src/app/screens/Accounts/Detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function AccountDetail() {
const [account, setAccount] = useState<GetAccountRes | null>(null);
const [accountName, setAccountName] = useState("");
const [hasMnemonic, setHasMnemonic] = useState(false);
const [isMnemonicBackupDone, setIsMnemonicBackupDone] = useState(false);
const [nostrPublicKey, setNostrPublicKey] = useState("");
const [hasImportedNostrKey, setHasImportedNostrKey] = useState(false);

Expand Down Expand Up @@ -80,6 +81,7 @@ function AccountDetail() {
setAccount(response);
setAccountName(response.name);
setHasMnemonic(response.hasMnemonic);
setIsMnemonicBackupDone(response.isMnemonicBackupDone);
setHasImportedNostrKey(response.hasImportedNostrKey);

if (response.nostrEnabled) {
Expand Down Expand Up @@ -312,7 +314,7 @@ function AccountDetail() {
</h2>

<div className="shadow bg-white rounded-md sm:overflow-hidden p-6 dark:bg-surface-01dp flex flex-col gap-4">
{hasMnemonic && (
{hasMnemonic && !isMnemonicBackupDone && (
<Alert type="warn">{t("mnemonic.backup.warning")}</Alert>
)}

Expand Down
Loading

0 comments on commit d8efb71

Please sign in to comment.