From a27201593cdfaaf48f55ba5961ba82de1ba6d066 Mon Sep 17 00:00:00 2001 From: secondl1ght Date: Tue, 24 Sep 2024 13:04:40 -0600 Subject: [PATCH 1/2] refactor: wallet settings redesign --- messages/en.json | 41 +- messages/es.json | 41 +- package-lock.json | 92 ++-- .../wallet/[walletId]/settings/page.tsx | 15 - .../(app)/(layout)/wallet/settings/page.tsx | 7 + src/components/button/VaultButton.tsx | 2 + src/components/button/VaultButtonV2.tsx | 520 ++++++++++++++++++ src/components/button/WalletButton.tsx | 2 +- src/utils/routes.ts | 2 +- src/views/wallet/Settings.tsx | 391 ++++++------- src/views/wallet/WalletInfo.tsx | 2 +- 11 files changed, 843 insertions(+), 272 deletions(-) delete mode 100644 src/app/(app)/(layout)/wallet/[walletId]/settings/page.tsx create mode 100644 src/app/(app)/(layout)/wallet/settings/page.tsx create mode 100644 src/components/button/VaultButtonV2.tsx diff --git a/messages/en.json b/messages/en.json index 252c4485..73577846 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,6 +1,5 @@ { "App": { - "copy": "Copy", "Dashboard": { "asset": "An asset in your wallet.", "buy": "Buy", @@ -14,17 +13,38 @@ "warning-title": "Add Bitcoin!", "what-asset": "What is {asset}?" }, - "miban": "MIBAN Code", - "save": "Save", - "view-all": "View all", "Wallet": { + "Settings": { + "decrypt": "Decrypt to view your wallet’s secret mnemonic.", + "decrypted": "Decrypted Mnemonic", + "descriptor": "Descriptor", + "miban": "Share this code with other people to receive payments.", + "name": "Wallet Name", + "settings": "Wallet Settings", + "tech-info": "Technical information about your wallet, helpful for advanced recovery.", + "unlock": "Unlock to Decrypt" + }, + "Vault": { + "lock": "Lock", + "lock-wallet": "Lock your wallet", + "locked": "Locked", + "password": "Your BancoLibre Password", + "seed": "Your seed phrase is encrypted with your password.", + "unlock": "Unlock", + "unlock-wallet": "Unlock your wallet", + "unlocked": "Unlocked" + }, "amount": "Please specify an amount.", "amount-custom": "Customize Amount", "amount-ln": "Please specify an amount to generate a Lightning invoice.", "assets": "Assets", "receive": "Receive", "send": "Send" - } + }, + "copy": "Copy", + "miban": "MIBAN Code", + "save": "Save", + "view-all": "View all" }, "Common": { "clear": "Clear Memory", @@ -34,6 +54,7 @@ "confirm-password": "Confirm Password", "decrypt": "Decrypt", "email": "Email", + "error": "Error", "mnemonic": "Encrypted Mnemonic", "next": "Next", "optional": "Optional", @@ -91,15 +112,12 @@ "secure": "Your account is secured with an authentication code. Open up the Authenticator app and enter the code here.", "with-passkey": "Log in with Passkey" }, - "login": "Log In", - "privacy": "Privacy Policy", "Recover": { "decrypted": "Decrypted Mnemonic (decrypt to show)", "description": "If you know your encrypted mnemonic, it can be decrypted in combination with email, password and symmetric key.", "recover": "Recover your wallet", "symmetric": "Symmetric Key" }, - "signup": "Sign up", "Signup": { "accept-password": "I understand that if I forget the password my account cannot be recovered.", "accept-terms": "By checking this box you agree to the Terms of Service and the Privacy Policy.", @@ -114,7 +132,6 @@ "set": "Set a password", "submit": "Create Account" }, - "terms": "Terms & Conditions", "Waitlist": { "good-things": "Good things take time. We’d love for you to be the first to know when we’re live!", "join": "Join our waitlist", @@ -122,6 +139,10 @@ "referral": "I have a referral code", "submit": "Join Waitlist", "subscriber": "I am an Amboss subscriber" - } + }, + "login": "Log In", + "privacy": "Privacy Policy", + "signup": "Sign up", + "terms": "Terms & Conditions" } } diff --git a/messages/es.json b/messages/es.json index 06788f3e..204c19f3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1,6 +1,5 @@ { "App": { - "copy": "Copiar", "Dashboard": { "asset": "Un activo en tu billetera.", "buy": "Comprar", @@ -14,17 +13,38 @@ "warning-title": "Agrega Bitcoin!", "what-asset": "Qué es {asset}?" }, - "miban": "Código MIBAN", - "save": "Guardar", - "view-all": "Ver todos", "Wallet": { + "Settings": { + "decrypt": "Decrypt to view your wallet’s secret mnemonic.", + "decrypted": "Decrypted Mnemonic", + "descriptor": "Descriptor", + "miban": "Share this code with other people to receive payments.", + "name": "Wallet Name", + "settings": "Wallet Settings", + "tech-info": "Technical information about your wallet, helpful for advanced recovery.", + "unlock": "Unlock to Decrypt" + }, + "Vault": { + "lock": "Lock", + "lock-wallet": "Lock your wallet", + "locked": "Locked", + "password": "Your BancoLibre Password", + "seed": "Your seed phrase is encrypted with your password.", + "unlock": "Unlock", + "unlock-wallet": "Unlock your wallet", + "unlocked": "Unlocked" + }, "amount": "Especifíca una cantidad.", "amount-custom": "Cambia la cantidad", "amount-ln": "Por favor especifíca una cantidad para generar un recibo de Lightning.", "assets": "Activos", "receive": "Recibir", "send": "Envíar" - } + }, + "copy": "Copiar", + "miban": "Código MIBAN", + "save": "Guardar", + "view-all": "Ver todos" }, "Common": { "clear": "Borrar de memoria", @@ -34,6 +54,7 @@ "confirm-password": "Repite la Contraseña", "decrypt": "Descifra", "email": "Correo", + "error": "Error", "mnemonic": "Mnemonic Cifrada", "next": "Siguiente", "optional": "Opcional", @@ -91,15 +112,12 @@ "secure": "Tu cuenta esta asegurada con un código de autenticación. Abre la app y introduce el código acá.", "with-passkey": "Iniciar sesión con un Passkey" }, - "login": "Iniciar Sesión", - "privacy": "Política de Privacidad", "Recover": { "decrypted": "Mnemonic Descifrada", "description": "Si sabes tu mnemonic cifrada, junto con tu correo, contraseña y llave simétrica, puedes descrifrarla.", "recover": "Recupera tu billetera", "symmetric": "Llave Simétrica" }, - "signup": "Registrarse", "Signup": { "accept-password": "Entiendo que si olvido mi contraseña, la cuenta no se puede recuperar.", "accept-terms": "Confirmo que acepto los Términos de Servicio y la Política de Privacidad.", @@ -114,7 +132,6 @@ "set": "Crea una contraseña", "submit": "Crea una cuenta" }, - "terms": "Términos y Condiciones", "Waitlist": { "good-things": "Las cosas buenas toman tiempo. Nos encantaría avisarte apenas estemos listos!", "join": "Uneté a nuestra lista de espera", @@ -122,6 +139,10 @@ "referral": "Tengo un código de invitación", "submit": "Uneté a nuestra lista de espera", "subscriber": "Tengo una subscripción de Amboss" - } + }, + "login": "Iniciar Sesión", + "privacy": "Política de Privacidad", + "signup": "Registrarse", + "terms": "Términos y Condiciones" } } diff --git a/package-lock.json b/package-lock.json index 054f3558..161b23ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4489,9 +4489,9 @@ "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==" }, "node_modules/@next/env": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.4.tgz", - "integrity": "sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==" + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz", + "integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.4", @@ -4503,9 +4503,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz", - "integrity": "sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.13.tgz", + "integrity": "sha512-IkAmQEa2Htq+wHACBxOsslt+jMoV3msvxCn0WFSfJSkv/scy+i/EukBKNad36grRxywaXUYJc9mxEGkeIs8Bzg==", "cpu": [ "arm64" ], @@ -4518,9 +4518,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", - "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.13.tgz", + "integrity": "sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==", "cpu": [ "x64" ], @@ -4533,9 +4533,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", - "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.13.tgz", + "integrity": "sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==", "cpu": [ "arm64" ], @@ -4548,9 +4548,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", - "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.13.tgz", + "integrity": "sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==", "cpu": [ "arm64" ], @@ -4563,9 +4563,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", - "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.13.tgz", + "integrity": "sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==", "cpu": [ "x64" ], @@ -4578,9 +4578,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", - "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.13.tgz", + "integrity": "sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==", "cpu": [ "x64" ], @@ -4593,9 +4593,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", - "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.13.tgz", + "integrity": "sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==", "cpu": [ "arm64" ], @@ -4608,9 +4608,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", - "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz", + "integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==", "cpu": [ "ia32" ], @@ -4623,9 +4623,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", - "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.13.tgz", + "integrity": "sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==", "cpu": [ "x64" ], @@ -9612,9 +9612,9 @@ } }, "node_modules/dset": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", - "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", "dev": true, "engines": { "node": ">=4" @@ -14409,11 +14409,11 @@ } }, "node_modules/next": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", - "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.13.tgz", + "integrity": "sha512-BseY9YNw8QJSwLYD7hlZzl6QVDoSFHL/URN5K64kVEVpCsSOWeyjbIGK+dZUaRViHTaMQX8aqmnn0PHBbGZezg==", "dependencies": { - "@next/env": "14.2.4", + "@next/env": "14.2.13", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -14428,15 +14428,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.4", - "@next/swc-darwin-x64": "14.2.4", - "@next/swc-linux-arm64-gnu": "14.2.4", - "@next/swc-linux-arm64-musl": "14.2.4", - "@next/swc-linux-x64-gnu": "14.2.4", - "@next/swc-linux-x64-musl": "14.2.4", - "@next/swc-win32-arm64-msvc": "14.2.4", - "@next/swc-win32-ia32-msvc": "14.2.4", - "@next/swc-win32-x64-msvc": "14.2.4" + "@next/swc-darwin-arm64": "14.2.13", + "@next/swc-darwin-x64": "14.2.13", + "@next/swc-linux-arm64-gnu": "14.2.13", + "@next/swc-linux-arm64-musl": "14.2.13", + "@next/swc-linux-x64-gnu": "14.2.13", + "@next/swc-linux-x64-musl": "14.2.13", + "@next/swc-win32-arm64-msvc": "14.2.13", + "@next/swc-win32-ia32-msvc": "14.2.13", + "@next/swc-win32-x64-msvc": "14.2.13" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/src/app/(app)/(layout)/wallet/[walletId]/settings/page.tsx b/src/app/(app)/(layout)/wallet/[walletId]/settings/page.tsx deleted file mode 100644 index ff33b86c..00000000 --- a/src/app/(app)/(layout)/wallet/[walletId]/settings/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useTranslations } from 'next-intl'; - -import { WalletSettings } from '@/views/wallet/Settings'; - -export default function Page({ params }: { params: { walletId: string } }) { - const t = useTranslations('Index'); - - return ( -
-

{t('settings')}

- - -
- ); -} diff --git a/src/app/(app)/(layout)/wallet/settings/page.tsx b/src/app/(app)/(layout)/wallet/settings/page.tsx new file mode 100644 index 00000000..8dd0ae49 --- /dev/null +++ b/src/app/(app)/(layout)/wallet/settings/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { WalletSettings } from '@/views/wallet/Settings'; + +export default function Page() { + return ; +} diff --git a/src/components/button/VaultButton.tsx b/src/components/button/VaultButton.tsx index e9a9021c..fff741d4 100644 --- a/src/components/button/VaultButton.tsx +++ b/src/components/button/VaultButton.tsx @@ -381,6 +381,8 @@ const PasskeyVaultButton: FC<{ title: 'Unable to unlock.', description: messages.join(', '), }); + + return; } setKeys({ diff --git a/src/components/button/VaultButtonV2.tsx b/src/components/button/VaultButtonV2.tsx new file mode 100644 index 00000000..03044c88 --- /dev/null +++ b/src/components/button/VaultButtonV2.tsx @@ -0,0 +1,520 @@ +'use client'; + +import { ApolloError, useApolloClient } from '@apollo/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { startAuthentication } from '@simplewebauthn/browser'; +import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; +import { Loader2, Lock, Unlock, X } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { FC, useEffect, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { + CheckPasswordDocument, + CheckPasswordMutation, + CheckPasswordMutationVariables, +} from '@/graphql/mutations/__generated__/checkPassword.generated'; +import { + LoginPasskeyAuthDocument, + LoginPasskeyAuthMutation, + LoginPasskeyAuthMutationVariables, + useLoginPasskeyInitAuthMutation, +} from '@/graphql/mutations/__generated__/passkey.generated'; +import { useUserQuery } from '@/graphql/queries/__generated__/user.generated'; +import { useKeyStore } from '@/stores/keys'; +import { toWithError } from '@/utils/async'; +import { cn } from '@/utils/cn'; +import { handleApolloError } from '@/utils/error'; +import { + cleanupWebauthnAuthenticationResponse, + getPRFSalt, +} from '@/utils/passkey'; +import { WorkerMessage, WorkerResponse } from '@/workers/account/types'; + +import { Button } from '../ui/button-v2'; +import { Drawer, DrawerContent, DrawerTrigger } from '../ui/drawer'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form'; +import { Input } from '../ui/input'; +import { useToast } from '../ui/use-toast'; + +type Variants = 'primary' | 'secondary' | 'neutral' | null | undefined; + +const formSchema = z.object({ + password: z.string(), +}); + +const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { + const t = useTranslations('App.Wallet.Vault'); + + const workerRef = useRef(); + + const client = useApolloClient(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + password: '', + }, + }); + + const { toast } = useToast(); + + const setKeys = useKeyStore(s => s.setKeys); + + const [loading, setLoading] = useState(false); + + const { + data, + loading: userLoading, + error, + } = useUserQuery({ + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting user data.', + description: messages.join(', '), + }); + }, + }); + + const handleSubmit = (values: z.infer) => { + if ( + loading || + userLoading || + error || + !data?.user.email || + !data?.user.protected_symmetric_key || + !workerRef.current + ) { + return; + } + + setLoading(true); + + const message: WorkerMessage = { + type: 'generateMaster', + payload: { + email: data.user.email, + password: values.password, + protectedSymmetricKey: data.user.protected_symmetric_key, + }, + }; + + workerRef.current.postMessage(message); + }; + + useEffect(() => { + workerRef.current = new Worker( + new URL('../../workers/account/account.ts', import.meta.url) + ); + + workerRef.current.onmessage = async event => { + const message: WorkerResponse = event.data; + + switch (message.type) { + case 'generateMaster': { + const [, error] = await toWithError( + client.mutate< + CheckPasswordMutation, + CheckPasswordMutationVariables + >({ + mutation: CheckPasswordDocument, + variables: { password: message.payload.masterPasswordHash }, + }) + ); + + if (error) { + const messages = handleApolloError(error as ApolloError); + + toast({ + variant: 'destructive', + title: 'Unable to unlock.', + description: messages.join(', '), + }); + + form.reset(); + } else { + setKeys({ + masterKey: message.payload.masterKey, + protectedSymmetricKey: message.payload.protectedSymmetricKey, + }); + callback(); + } + break; + } + + case 'error': + toast({ + variant: 'destructive', + title: 'Error unlocking.', + description: message.msg, + }); + break; + } + + setLoading(false); + }; + + workerRef.current.onerror = error => { + console.error('Worker error:', error); + setLoading(false); + }; + + return () => { + if (workerRef.current) workerRef.current.terminate(); + }; + }, [client, toast, callback, form, setKeys]); + + return ( + <> +

{t('unlock-wallet')}

+ +

{t('seed')}

+ +
+ { + form.handleSubmit(handleSubmit)(event); + + event?.preventDefault?.(); + event?.stopPropagation?.(); + }} + className="space-y-4" + > + ( + + {t('password')} + + + + + + )} + /> + + + + + + ); +}; + +const VaultPasswordButton: FC<{ + lockedTitle: string; + className?: string; + variant?: Variants; + size?: 'md'; +}> = ({ lockedTitle, className, variant, size }) => { + const t = useTranslations('App.Wallet.Vault'); + + const keys = useKeyStore(s => s.keys); + + const clearKeys = useKeyStore(s => s.clear); + + const [open, setOpen] = useState(false); + + const handleClear = () => { + clearKeys(); + setOpen(false); + }; + + return ( + + + + + + + {!!keys ? ( + <> +

{t('lock-wallet')}

+ +

+ {t('seed')} +

+ + + + ) : ( + { + setOpen(false); + }} + /> + )} +
+
+ ); +}; + +const PasskeyVaultButton: FC<{ + lockedTitle: string; + className?: string; + variant?: Variants; + size?: 'md'; + protectedSymmetricKey: string; + passkeyId: string; +}> = ({ + lockedTitle, + className, + variant, + size, + protectedSymmetricKey, + passkeyId, +}) => { + const t = useTranslations('App.Wallet.Vault'); + + const client = useApolloClient(); + + const keys = useKeyStore(s => s.keys); + const setKeys = useKeyStore(s => s.setKeys); + const clearKeys = useKeyStore(s => s.clear); + + const [loading, setLoading] = useState(false); + + const { toast } = useToast(); + + const [setup, { loading: addLoading }] = useLoginPasskeyInitAuthMutation({ + onCompleted: data => { + try { + handleAuthentication(JSON.parse(data.passkey.init_authenticate)); + } catch (error) { + toast({ + variant: 'destructive', + title: 'Error enabling encryption for Passkey.', + }); + } + }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting Passkey details.', + description: messages.join(', '), + }); + }, + }); + + const handleAuthentication = async ( + options: PublicKeyCredentialRequestOptionsJSON + ) => { + setLoading(true); + + try { + const originalResponse = await startAuthentication({ + ...options, + extensions: { + ...options.extensions, + prf: { eval: { first: await getPRFSalt() } }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }); + + const { response, prfSecretHash } = + await cleanupWebauthnAuthenticationResponse(originalResponse); + + if (!prfSecretHash) { + throw new Error('This Passkey does not have encryption capabilities.'); + } + + if ('prf' in response.clientExtensionResults) { + alert( + 'PRF result should never be sent to the server. This should only happen if a developer made a mistake.' + ); + return; + } + + const [, error] = await toWithError( + client.mutate< + LoginPasskeyAuthMutation, + LoginPasskeyAuthMutationVariables + >({ + mutation: LoginPasskeyAuthDocument, + variables: { + input: { + options: JSON.stringify(response), + }, + }, + }) + ); + + if (error) { + const messages = handleApolloError(error as ApolloError); + + toast({ + variant: 'destructive', + title: 'Unable to unlock.', + description: messages.join(', '), + }); + + return; + } + + setKeys({ + masterKey: prfSecretHash, + protectedSymmetricKey, + }); + } catch (error) { + toast({ + variant: 'destructive', + title: 'Error Unlocking.', + description: error instanceof Error ? error.message : undefined, + }); + } finally { + setLoading(false); + } + }; + + if (!keys) { + return ( + + ); + } + + return ( + + ); +}; + +export const VaultButton: FC<{ + lockedTitle?: string; + className?: string; + variant?: Variants; + size?: 'md'; +}> = ({ lockedTitle, className, variant, size }) => { + const t = useTranslations(); + + const lockedTitleFinal = lockedTitle || t('App.Wallet.Vault.locked'); + + const { data, loading, error } = useUserQuery(); + + if (loading) { + return ( + + ); + } + + if (error || !data?.user.protected_symmetric_key) { + return ( + + ); + } + + if (data.user.using_passkey_id) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/src/components/button/WalletButton.tsx b/src/components/button/WalletButton.tsx index 71490198..15c12267 100644 --- a/src/components/button/WalletButton.tsx +++ b/src/components/button/WalletButton.tsx @@ -139,7 +139,7 @@ export function WalletButton() { }} > Settings diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 82663493..07af24ac 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -15,7 +15,7 @@ export const ROUTES = { }, wallet: { home: '/wallet', - settings: (id: string) => `/wallet/${id}/settings`, + settings: '/wallet/settings', receive: '/wallet/receive', send: { home: (walletId: string, accountId: string, assetId?: string) => diff --git a/src/views/wallet/Settings.tsx b/src/views/wallet/Settings.tsx index bbfd5564..18e6a666 100644 --- a/src/views/wallet/Settings.tsx +++ b/src/views/wallet/Settings.tsx @@ -1,10 +1,13 @@ 'use client'; -import { Copy, CopyCheck, Loader2 } from 'lucide-react'; -import { FC, useEffect, useRef, useState } from 'react'; - -import { VaultButton } from '@/components/button/VaultButton'; -import { Button } from '@/components/ui/button'; +import { ArrowLeft, Copy, CopyCheck, CornerDownRight } from 'lucide-react'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; +import { FC, useEffect, useMemo, useRef, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; + +import { VaultButton } from '@/components/button/VaultButtonV2'; +import { Button } from '@/components/ui/button-v2'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useToast } from '@/components/ui/use-toast'; @@ -12,25 +15,27 @@ import { useChangeWalletNameMutation } from '@/graphql/mutations/__generated__/w import { useGetWalletDetailsQuery } from '@/graphql/queries/__generated__/wallet.generated'; import useCopyClipboard from '@/hooks/useClipboardCopy'; import { useKeyStore } from '@/stores/keys'; +import { LOCALSTORAGE_KEYS } from '@/utils/constants'; import { handleApolloError } from '@/utils/error'; +import { ROUTES } from '@/utils/routes'; import { CryptoWorkerMessage, CryptoWorkerResponse, } from '@/workers/crypto/types'; -import { Section } from '../settings/Section'; - const WalletName: FC<{ walletId: string }> = ({ walletId }) => { + const t = useTranslations('App'); + const { toast } = useToast(); - const { data } = useGetWalletDetailsQuery({ + const { data, loading } = useGetWalletDetailsQuery({ variables: { id: walletId }, errorPolicy: 'ignore', }); const [name, setName] = useState(data?.wallets.find_one.name || ''); - const [changeName, { loading }] = useChangeWalletNameMutation({ + const [changeName, { loading: loadingChange }] = useChangeWalletNameMutation({ onCompleted: () => { toast({ title: 'Wallet name saved.', @@ -53,49 +58,85 @@ const WalletName: FC<{ walletId: string }> = ({ walletId }) => { setName(data.wallets.find_one.name); }, [data]); - if (!data) return null; + const hasChange = name !== data?.wallets.find_one.name; + + return ( +
+ + +
+ setName(e.target.value)} + disabled={loading || loadingChange} + /> + + +
+
+ ); +}; - const hasChange = name !== data.wallets.find_one.name; +const WalletCode: FC<{ walletId: string }> = ({ walletId }) => { + const t = useTranslations('App'); + + const [copiedText, copy] = useCopyClipboard(); + + const { data } = useGetWalletDetailsQuery({ + variables: { id: walletId }, + errorPolicy: 'ignore', + }); + + const address = useMemo(() => { + if (!data?.wallets.find_one.money_address.length) return ''; + + const first = data.wallets.find_one.money_address[0]; + + return first.user + '@' + first.domains[0]; + }, [data]); return ( -
-
- -
- setName(e.target.value)} - placeholder="Wallet Name" - /> - -
+
+ + +

+ {t('Wallet.Settings.miban')} +

+ +
+ + +
-
+ ); }; const WalletMnemonic: FC<{ walletId: string }> = ({ walletId }) => { + const t = useTranslations(); + const { toast } = useToast(); const workerRef = useRef(); - const [stateLoading, setLoading] = useState(false); - - const [protectedMnemonic, setProtectedMnemonic] = useState(''); const [mnemonic, setMnemonic] = useState(''); + const [loading, setLoading] = useState(false); + + const [copiedMnemonic, copyMnemonic] = useCopyClipboard(); const keys = useKeyStore(s => s.keys); @@ -108,10 +149,26 @@ const WalletMnemonic: FC<{ walletId: string }> = ({ walletId }) => { }), }); - useEffect(() => { - if (!data?.wallets.find_one.details.protected_mnemonic) return; - setProtectedMnemonic(data.wallets.find_one.details.protected_mnemonic); - }, [data]); + const protectedMnemonic = + data?.wallets.find_one.details.protected_mnemonic || ''; + + const disabled = loading || walletLoading; + + const handleDecrypt = () => { + if (!keys || !protectedMnemonic || !workerRef.current) return; + + setLoading(true); + + const message: CryptoWorkerMessage = { + type: 'decryptMnemonic', + payload: { + protectedMnemonic, + keys, + }, + }; + + workerRef.current.postMessage(message); + }; useEffect(() => { workerRef.current = new Worker( @@ -125,13 +182,13 @@ const WalletMnemonic: FC<{ walletId: string }> = ({ walletId }) => { case 'decryptMnemonic': setMnemonic(message.payload.mnemonic); break; + case 'error': toast({ variant: 'destructive', title: 'Error decrypting mnemonic.', description: `Please reach out to support. ${message.msg}`, }); - break; } @@ -148,184 +205,142 @@ const WalletMnemonic: FC<{ walletId: string }> = ({ walletId }) => { }; }, [toast]); - const handleDecrypt = () => { - if (!keys) return; - if (!data?.wallets.find_one.details.protected_mnemonic) return; - - setLoading(true); - - if (workerRef.current) { - const message: CryptoWorkerMessage = { - type: 'decryptMnemonic', - payload: { - protectedMnemonic: data.wallets.find_one.details.protected_mnemonic, - keys, - }, - }; - - workerRef.current.postMessage(message); - } - }; - - const [copiedMnemonic, copyMnemonic] = useCopyClipboard(); - - const loading = stateLoading || walletLoading; - return ( -
+
- -
+ + +

+ {t('App.Wallet.Settings.decrypt')} +

+ +
+ {!keys ? ( - + ) : ( )}
-
- -
- - +
+
+ + + +
+ +

+ {t('Common.private')} +

+ +
+
+ + + +
+
-
+ ); }; -export const WalletSettings: FC<{ walletId: string }> = ({ walletId }) => { - const { data, loading } = useGetWalletDetailsQuery({ +const WalletDescriptor: FC<{ walletId: string }> = ({ walletId }) => { + const t = useTranslations('App.Wallet.Settings'); + + const [copiedText, copy] = useCopyClipboard(); + + const { data } = useGetWalletDetailsQuery({ variables: { id: walletId }, errorPolicy: 'ignore', }); - const [copiedText, copy] = useCopyClipboard(); + return ( +
+ {data?.wallets.find_one.accounts.map((a, i) => { + return ( +
+ + +

{t('tech-info')}

+ +
+ + + +
+
+ ); + })} +
+ ); +}; - if (loading) { - return ( -
- -
- ); - } +export const WalletSettings = () => { + const t = useTranslations('App.Wallet.Settings'); - if (!data?.wallets.find_one.id) { - return ( -
-

Error loading wallet.

-
- ); - } + const [walletId] = useLocalStorage(LOCALSTORAGE_KEYS.currentWalletId, ''); return ( -
- -
- {!data?.wallets.find_one.money_address.length ? ( -

No MIBAN Code found.

- ) : ( - data.wallets.find_one.money_address.map(a => { - return a.domains.map(d => { - return ( -
- -
- - -
-
- ); - }); - }) - )} -
- - - -
- {!data?.wallets.find_one.accounts.length ? ( -

No accounts found.

- ) : ( - data.wallets.find_one.accounts.map((a, i) => { - return ( -
- -
- - -
-
- ); - }) - )} -
+
+
+ + + + +

{t('settings')}

+
+ +
+ + + + +
); }; diff --git a/src/views/wallet/WalletInfo.tsx b/src/views/wallet/WalletInfo.tsx index 18a51dff..d8f94431 100644 --- a/src/views/wallet/WalletInfo.tsx +++ b/src/views/wallet/WalletInfo.tsx @@ -420,7 +420,7 @@ export const WalletInfo: FC<{ From fbe680b088adaaa59c68909079486cbac87c7c48 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin Date: Wed, 25 Sep 2024 09:45:24 -0600 Subject: [PATCH 2/2] chore: lang strings --- messages/es.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/messages/es.json b/messages/es.json index 0d773aa1..ec9fa321 100644 --- a/messages/es.json +++ b/messages/es.json @@ -15,24 +15,24 @@ }, "Wallet": { "Settings": { - "decrypt": "Decrypt to view your wallet’s secret mnemonic.", - "decrypted": "Decrypted Mnemonic", + "decrypt": "Desencriptar para ver la frase secreta de tu billetera.", + "decrypted": "Frase Secreta Desencriptada", "descriptor": "Descriptor", - "miban": "Share this code with other people to receive payments.", - "name": "Wallet Name", - "settings": "Wallet Settings", - "tech-info": "Technical information about your wallet, helpful for advanced recovery.", - "unlock": "Unlock to Decrypt" + "miban": "Comparte este código con otras personas para recibir pagos.", + "name": "Nombre de la Billetera", + "settings": "Configuración de la Billetera", + "tech-info": "Información técnica sobre tu billetera, útil para recuperación avanzada.", + "unlock": "Desbloquear para Desencriptar" }, "Vault": { - "lock": "Lock", - "lock-wallet": "Lock your wallet", - "locked": "Locked", - "password": "Your BancoLibre Password", - "seed": "Your seed phrase is encrypted with your password.", - "unlock": "Unlock", - "unlock-wallet": "Unlock your wallet", - "unlocked": "Unlocked" + "lock": "Bloquear", + "lock-wallet": "Bloquear tu billetera", + "locked": "Bloqueado", + "password": "Tu Contraseña de BancoLibre", + "seed": "Tu frase semilla está encriptada con tu contraseña.", + "unlock": "Desbloquear", + "unlock-wallet": "Desbloquear tu billetera", + "unlocked": "Desbloqueado" }, "add-contact": "Agregar Contacto", "add-desc": "Agrega una descripción",