diff --git a/.changeset/chilly-poets-beg.md b/.changeset/chilly-poets-beg.md new file mode 100644 index 000000000..70af5fcd3 --- /dev/null +++ b/.changeset/chilly-poets-beg.md @@ -0,0 +1,6 @@ +--- +'@status-im/wallet': patch +'wallet': patch +--- + +add new wallet flow diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 279750f42..9046ea7ce 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,8 @@ "mikestead.dotenv", "bradlc.vscode-tailwindcss", "vitest.explorer", - "github.vscode-github-actions" + "github.vscode-github-actions", + "eamodio.gitlens", + "github.vscode-pull-request-github" ] } diff --git a/apps/portfolio/src/app/_components/navbar.tsx b/apps/portfolio/src/app/_components/navbar.tsx index b43f94bcf..8dfe967c2 100644 --- a/apps/portfolio/src/app/_components/navbar.tsx +++ b/apps/portfolio/src/app/_components/navbar.tsx @@ -1,12 +1,9 @@ 'use client' import { Navbar as NavbarBase } from '@status-im/wallet/components' -import { usePathname } from 'next/navigation' const Navbar = () => { - const pathname = usePathname() - - return + return } export { Navbar } diff --git a/apps/wallet/package.json b/apps/wallet/package.json index d08f7cde8..8deb47d2a 100644 --- a/apps/wallet/package.json +++ b/apps/wallet/package.json @@ -30,6 +30,7 @@ "@cardano-sdk/core": "^0.45.4", "@cardano-sdk/crypto": "^0.2.3", "@cardano-sdk/key-management": "^0.27.5", + "@hookform/resolvers": "^3.1.1", "@radix-ui/react-dialog": "^1.1.1", "@status-im/colors": "workspace:*", "@status-im/components": "workspace:*", diff --git a/apps/wallet/src/data/api.ts b/apps/wallet/src/data/api.ts index 2ff7d8543..ea3564138 100644 --- a/apps/wallet/src/data/api.ts +++ b/apps/wallet/src/data/api.ts @@ -573,7 +573,7 @@ const apiRouter = router({ // ) const { id } = await keyStore.importKey( - Buffer.from(input.privateKey), + new Uint8Array(Buffer.from(input.privateKey)), input.name, input.password, walletCore.CoinType.ethereum, diff --git a/apps/wallet/src/hooks/use-create-wallet.tsx b/apps/wallet/src/hooks/use-create-wallet.tsx index fd6561980..f5c953c5f 100644 --- a/apps/wallet/src/hooks/use-create-wallet.tsx +++ b/apps/wallet/src/hooks/use-create-wallet.tsx @@ -1,9 +1,10 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAPI } from '../providers/api-client' export const useCreateWallet = () => { const api = useAPI() + const queryClient = useQueryClient() const { mutate, mutateAsync, ...result } = useMutation({ mutationKey: ['create-wallet'], @@ -15,6 +16,9 @@ export const useCreateWallet = () => { return mnemonic }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['wallets'] }) + }, }) return { diff --git a/apps/wallet/src/hooks/use-import-wallet.tsx b/apps/wallet/src/hooks/use-import-wallet.tsx index 6d2afa7d9..b74090c3e 100644 --- a/apps/wallet/src/hooks/use-import-wallet.tsx +++ b/apps/wallet/src/hooks/use-import-wallet.tsx @@ -1,9 +1,10 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAPI } from '../providers/api-client' export const useImportWallet = () => { const api = useAPI() + const queryClient = useQueryClient() const { mutate, mutateAsync, ...result } = useMutation({ mutationKey: ['import-wallet'], @@ -20,6 +21,9 @@ export const useImportWallet = () => { name: 'Imported Wallet', }) }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['wallets'] }) + }, }) return { diff --git a/apps/wallet/src/hooks/use-pin-extension.tsx b/apps/wallet/src/hooks/use-pin-extension.tsx new file mode 100644 index 000000000..29f43d6ca --- /dev/null +++ b/apps/wallet/src/hooks/use-pin-extension.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' + +export const usePinExtension = () => { + const [isPinExtension, setIsPinExtension] = useState(false) + + const handleClose = async () => { + setIsPinExtension(false) + await chrome.storage.local.set({ pinExtension: true }) + } + + useEffect(() => { + async function checkSettings() { + const storage = await chrome.storage.local.get(['pinExtension']) + if (storage.pinExtension) { + setIsPinExtension(false) + return + } + + const settings = await chrome.action.getUserSettings() + setIsPinExtension(!settings.isOnToolbar) + } + + checkSettings() + }, []) + + return { isPinExtension, handleClose } +} diff --git a/apps/wallet/src/providers/wallet-context.tsx b/apps/wallet/src/providers/wallet-context.tsx new file mode 100644 index 000000000..4924d650b --- /dev/null +++ b/apps/wallet/src/providers/wallet-context.tsx @@ -0,0 +1,76 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +import { useQuery } from '@tanstack/react-query' + +import { apiClient } from './api-client' + +import type { KeyStore } from '@trustwallet/wallet-core' + +type Wallet = KeyStore.Wallet + +type WalletContext = { + currentWallet: Wallet | null + wallets: Wallet[] + isLoading: boolean + hasWallets: boolean + setCurrentWallet: (id: Wallet['id']) => void +} + +const WalletContext = createContext(undefined) + +export function useWallet() { + const context = useContext(WalletContext) + if (!context) { + throw new Error('useWallet must be used within WalletProvider') + } + return context +} + +export function WalletProvider({ children }: { children: React.ReactNode }) { + const [selectedWalletId, setSelectedWalletId] = useState(null) + + const { data: wallets = [], isLoading } = useQuery({ + queryKey: ['wallets'], + queryFn: () => apiClient.wallet.all.query(), + staleTime: 5 * 60 * 1000, // 5 minutes + }) + + const hasWallets = wallets.length > 0 + + const currentWallet = useMemo(() => { + if (!hasWallets) return null + + if (selectedWalletId) { + const selectedWallet = wallets.find( + wallet => wallet.id === selectedWalletId, + ) + if (selectedWallet) return selectedWallet + } + + return wallets[0] || null + }, [hasWallets, selectedWalletId, wallets]) + + useEffect(() => { + if (hasWallets && !selectedWalletId && wallets[0]) { + setSelectedWalletId(wallets[0].id) + } + }, [hasWallets, selectedWalletId, wallets]) + + const setCurrentWallet = (id: string) => { + setSelectedWalletId(id) + } + + const contextValue: WalletContext = { + currentWallet, + wallets, + isLoading, + hasWallets, + setCurrentWallet, + } + + return ( + + {children} + + ) +} diff --git a/apps/wallet/src/public/images/onboarding.png b/apps/wallet/src/public/images/onboarding.png new file mode 100644 index 000000000..dd30139af Binary files /dev/null and b/apps/wallet/src/public/images/onboarding.png differ diff --git a/apps/wallet/src/routes/__root.tsx b/apps/wallet/src/routes/__root.tsx index 5cabef4d7..94fbf8533 100644 --- a/apps/wallet/src/routes/__root.tsx +++ b/apps/wallet/src/routes/__root.tsx @@ -19,6 +19,7 @@ import { TanStackRouterDevtools } from '@tanstack/router-devtools' // import { QueryClientProvider } from '../../../portfolio/src/app/_providers/query-client-provider' // import { StatusProvider } from '../../../portfolio/src/app/_providers/status-provider' import { WagmiProvider } from '../../../portfolio/src/app/_providers/wagmi-provider' +import { WalletProvider } from '../providers/wallet-context' // import { Inter } from 'next/font/google' import type { QueryClient } from '@tanstack/react-query' @@ -69,26 +70,28 @@ function RootComponent() { -
+
{/* */} {/* */} {/* Loading...
}> */} {/* */} {/* */} -
- -
-
-
-
- {/* */} - + +
+ +
+
+
+
+ {/* */} + +
+ {/* */} +
- {/* */} - -
+ {/* */} {/* */} {/* */} diff --git a/apps/wallet/src/routes/index.tsx b/apps/wallet/src/routes/index.tsx index 04f8acf2d..1bb24ca6a 100644 --- a/apps/wallet/src/routes/index.tsx +++ b/apps/wallet/src/routes/index.tsx @@ -1,4 +1,6 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' + +import { apiClient } from '../providers/api-client' export const Route = createFileRoute('/')({ component: RouteComponent, @@ -9,6 +11,12 @@ export const Route = createFileRoute('/')({ }, ], }), + beforeLoad: async () => { + const wallets = await apiClient.wallet.all.query() + if (wallets && wallets.length > 0) { + throw redirect({ to: '/portfolio' }) + } else throw redirect({ to: '/onboarding' }) + }, }) function RouteComponent() { diff --git a/apps/wallet/src/routes/onboarding/_layout.tsx b/apps/wallet/src/routes/onboarding/_layout.tsx index 86162a729..458d0be3c 100644 --- a/apps/wallet/src/routes/onboarding/_layout.tsx +++ b/apps/wallet/src/routes/onboarding/_layout.tsx @@ -1,13 +1,21 @@ -import { createFileRoute, Outlet } from '@tanstack/react-router' +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' + +import { apiClient } from '../../providers/api-client' export const Route = createFileRoute('/onboarding')({ component: RouteComponent, - beforeLoad: () => { - // TODO: check if user is already onboarded - // throw redirect({ to: '/' }) + beforeLoad: async () => { + const wallets = await apiClient.wallet.all.query() + if (wallets && wallets.length > 0) { + throw redirect({ to: '/portfolio' }) + } }, }) function RouteComponent() { - return + return ( +
+ +
+ ) } diff --git a/apps/wallet/src/routes/onboarding/index.tsx b/apps/wallet/src/routes/onboarding/index.tsx index 3aea6e426..007a16e13 100644 --- a/apps/wallet/src/routes/onboarding/index.tsx +++ b/apps/wallet/src/routes/onboarding/index.tsx @@ -1,4 +1,5 @@ -import { createFileRoute, Link } from '@tanstack/react-router' +import { Button, Text } from '@status-im/components' +import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/onboarding/')({ component: RouteComponent, @@ -13,19 +14,49 @@ export const Route = createFileRoute('/onboarding/')({ function RouteComponent() { return ( -
- - Create a wallet - - - I already have a wallet - +
+ Onboarding +
+ + Your Wallet. +
+ Your crypto. +
+ + Some awesome sub copy + +
+
+ + +
+ + By continuing you agree with Status +
+ + Terms of use + {' '} + and{' '} + + Privacy policy + +
) } diff --git a/apps/wallet/src/routes/onboarding/new.tsx b/apps/wallet/src/routes/onboarding/new.tsx index b290e82e3..7fef3ef0b 100644 --- a/apps/wallet/src/routes/onboarding/new.tsx +++ b/apps/wallet/src/routes/onboarding/new.tsx @@ -1,123 +1,55 @@ -import { useState } from 'react' +import { useTransition } from 'react' -import { Button, Input } from '@status-im/components' -import { createFileRoute } from '@tanstack/react-router' -import { useForm } from 'react-hook-form' +import { Button } from '@status-im/components' +import { ArrowLeftIcon } from '@status-im/icons/20' +import { CreatePasswordForm } from '@status-im/wallet/components' +import { createFileRoute, useNavigate } from '@tanstack/react-router' + +import { useCreateWallet } from '../../hooks/use-create-wallet' + +import type { CreatePasswordFormValues } from '@status-im/wallet/components' +import type { SubmitHandler } from 'react-hook-form' export const Route = createFileRoute('/onboarding/new')({ component: RouteComponent, }) -type OnboardingState = - | { type: 'create-password' } - | { type: 'recovery-phrase'; mnemonic: string } - function RouteComponent() { - const [onboardingState, setOnboardingState] = useState({ - type: 'create-password', - }) - - return ( -
- {onboardingState.type === 'create-password' && ( - - setOnboardingState({ type: 'recovery-phrase', mnemonic }) - } - /> - )} - {onboardingState.type === 'recovery-phrase' && ( - - )} -
- ) -} - -function CreatePassword({ onNext }: { onNext: (wallet: string) => void }) { - const { - register, - // handleSubmit, - formState: { errors }, - watch, - } = useForm({ - defaultValues: { - password: '', - confirmPassword: '', - }, - }) - const { createWalletAsync } = useCreateWallet() - - // const onSubmit = handleSubmit(async data => { - // // const wallet = await createWalletAsync(data.password) - // // console.log(wallet.mnemonic().split(' ')) - // // onNext(wallet) - // }) + const navigate = useNavigate() + const [isPending, startTransition] = useTransition() + + const handleSubmit: SubmitHandler = async data => { + try { + startTransition(async () => { + await createWalletAsync(data.password) + navigate({ to: '/portfolio' }) + }) + } catch (error) { + console.error(error) + } + } return ( -
- {/* @ts-expect-error: fixme: Types of property 'onChange' are incompatible. */} - - {errors.password && ( -

{errors.password.message}

- )} - {/* @ts-expect-error: fixme: Types of property 'onChange' are incompatible. */} - - value === watch('password') || 'Passwords do not match', - })} - /> - {errors.confirmPassword && ( -

- {errors.confirmPassword.message} -

- )} - -
- ) -} - -function RecoveryPhrase({ mnemonic }: { mnemonic: string }) { - return ( -
-

Write down your recovery phrase

-

Read the following carefully before continuing

- -
- {mnemonic.split(' ').map((word, i) => ( -
- {i + 1}. - {word} -
- ))} +
+
+
+
+

Create password

+
+ To unlock the extension and sign transactions, the password is stored + only on your device. Status can't recover it. +
+ +
- -
) } diff --git a/apps/wallet/src/routes/portfolio/index.tsx b/apps/wallet/src/routes/portfolio/index.tsx index ff0dbbee4..9758b6893 100644 --- a/apps/wallet/src/routes/portfolio/index.tsx +++ b/apps/wallet/src/routes/portfolio/index.tsx @@ -1,14 +1,25 @@ // import { Suspense } from 'react' -import { AssetsList } from '@status-im/wallet/components' +import { AssetsList, PinExtension } from '@status-im/wallet/components' import { useQuery } from '@tanstack/react-query' -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' + +import { usePinExtension } from '@/hooks/use-pin-extension' + +import { apiClient } from '../../providers/api-client' +import { useWallet } from '../../providers/wallet-context' // import { DetailDrawer } from '../../../../portfolio/src/app/[address]/@detail/_drawer' // import { Loading as LoadingNav } from '../../../../portfolio/src/app/[address]/@nav/loading' export const Route = createFileRoute('/portfolio/')({ component: RouteComponent, + beforeLoad: async () => { + const wallets = await apiClient.wallet.all.query() + if (!wallets || wallets.length === 0) { + throw redirect({ to: '/onboarding' }) + } + }, head: () => ({ meta: [ { @@ -19,6 +30,9 @@ export const Route = createFileRoute('/portfolio/')({ }) function RouteComponent() { + const { currentWallet, isLoading: isWalletLoading } = useWallet() + const { isPinExtension, handleClose } = usePinExtension() + const handleSelect = (url: string, options?: { scroll?: boolean }) => { // Handle the selection of an asset console.log('Selected asset URL:', url) @@ -28,8 +42,12 @@ function RouteComponent() { // todo: export trpc client with api router and used instead // todo: cache const { data: assets, isLoading } = useQuery({ - queryKey: ['assets'], + queryKey: ['assets', currentWallet?.activeAccounts[0].address], queryFn: async () => { + if (!currentWallet?.activeAccounts[0].address) { + throw new Error('No wallet address available') + } + const url = new URL( `${import.meta.env.WXT_STATUS_API_URL}/api/trpc/assets.all`, ) @@ -38,7 +56,7 @@ function RouteComponent() { // encodeURIComponent( JSON.stringify({ json: { - address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + address: currentWallet.activeAccounts[0].address, networks: [ 'ethereum', 'optimism', @@ -52,7 +70,6 @@ function RouteComponent() { // ), ) - // note: http://localhost:3030/api/trpc/assets.all?input={"json":{"address":"0xd8da6bf26964af9d7eed9e03e53415d37aa96045","networks":["ethereum","optimism","arbitrum","base","polygon","bsc"]}} const response = await fetch(url, { method: 'GET', headers: { @@ -61,13 +78,13 @@ function RouteComponent() { }) if (!response.ok) { - throw new Error('Failed to fetch.') + throw new Error('Failed to fetch assets.') } const body = await response.json() - return body.result.data.json.assets }, + enabled: !!currentWallet?.activeAccounts[0].address && !isWalletLoading, staleTime: 60 * 60 * 1000, // 1 hour gcTime: 60 * 60 * 1000, // 1 hour refetchOnMount: false, @@ -75,12 +92,16 @@ function RouteComponent() { refetchOnReconnect: false, }) + if (!currentWallet) { + return
No wallet selected
+ } + return (
- {isLoading ? ( + {isWalletLoading || isLoading ? (
Loading...
) : ( Detail
+ {isPinExtension && ( +
+ +
+ )}
) } diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 10f9b0b54..b1d48ff3c 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -52,7 +52,7 @@ "react": "^18.2.0" }, "dependencies": { - "@types/async-retry": "^1.4.9", + "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", @@ -67,23 +67,27 @@ "@status-im/colors": "workspace:*", "@status-im/components": "workspace:*", "@status-im/icons": "workspace:*", + "@trpc/react-query": "10.45.2", + "@trpc/server": "10.45.2", + "@types/async-retry": "^1.4.9", + "@zxcvbn-ts/core": "^3.0.4", "async-retry": "^1.3.3", "class-variance-authority": "^0.7.1", "cva": "^1.0.0-beta.1", "date-fns": "^2.30.0", - "@trpc/server": "10.45.2", - "@trpc/react-query": "10.45.2", "next": "15.1.6", "qrcode.react": "^3.1.0", "react-aria-components": "^1.3.3", "react-day-picker": "^8.7.1", + "react-hook-form": "^7.57.0", "react-swipeable": "^7.0.1", "superjson": "^2.2.1", "ts-pattern": "^5.3.1", - "zustand": "^4.3.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^4.3.7" }, "devDependencies": { + "@hookform/devtools": "^4.3.1", "@status-im/eslint-config": "workspace:*", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.1", diff --git a/packages/wallet/src/components/create-password-form/index.tsx b/packages/wallet/src/components/create-password-form/index.tsx new file mode 100644 index 000000000..af81f66ee --- /dev/null +++ b/packages/wallet/src/components/create-password-form/index.tsx @@ -0,0 +1,193 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { + Button, + // , Switch +} from '@status-im/components' +import { AlertIcon, InfoIcon, PositiveStateIcon } from '@status-im/icons/16' +import { LoadingIcon } from '@status-im/icons/20' +import { FormProvider, useForm } from 'react-hook-form' +import { z } from 'zod' + +import { + MAX_PASSWORD_LENGTH, + MIN_PASSWORD_LENGTH, + PasswordInput, +} from '../password-input' +import { PasswordStrength } from '../password-strength' + +const passwordSchema = z + .string() + .min( + MIN_PASSWORD_LENGTH, + `Password must be at least ${MIN_PASSWORD_LENGTH} characters long`, + ) + .max( + MAX_PASSWORD_LENGTH, + `Password must not exceed ${MAX_PASSWORD_LENGTH} characters`, + ) + .refine(password => /[a-z]/.test(password), { + message: 'Password must contain at least one lowercase letter', + }) + .refine(password => /[A-Z]/.test(password), { + message: 'Password must contain at least one uppercase letter', + }) + .refine(password => /[0-9]/.test(password), { + message: 'Password must contain at least one number', + }) + .refine(password => /[^a-zA-Z0-9]/.test(password), { + message: 'Password must contain at least one symbol', + }) + +const createPasswordSchema = z + .object({ + password: passwordSchema, + confirmPassword: z.string(), + isDefaultWallet: z.boolean().default(true), + }) + .refine(data => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }) + +type FormValues = z.infer + +type CreatePasswordFormProps = { + onSubmit: (data: FormValues) => void + loading: boolean +} + +const CreatePasswordForm = ({ + onSubmit, + loading = false, +}: CreatePasswordFormProps) => { + const form = useForm({ + resolver: zodResolver(createPasswordSchema), + mode: 'onBlur', + defaultValues: { + password: '', + confirmPassword: '', + isDefaultWallet: true, + }, + }) + + const { + formState: { errors }, + } = form + + const isPasswordValid = form.watch('password').length >= MIN_PASSWORD_LENGTH + const doPasswordsMatch = + form.watch('password') === form.watch('confirmPassword') + const confirmPassword = form.watch('confirmPassword') + + return ( + +
+
+
+ + +
+ {errors.password ? ( + <> + {errors.password.message} + + ) : isPasswordValid ? ( + <> + Minimum {MIN_PASSWORD_LENGTH} characters + + ) : ( + <> + Minimum {MIN_PASSWORD_LENGTH} characters + + )} +
+
+ +
+ + + {(confirmPassword || errors.confirmPassword) && ( +
+ {errors.confirmPassword ? ( + <> + {errors.confirmPassword.message} + + ) : doPasswordsMatch ? ( + <> + Passwords match + + ) : ( + <> + Passwords do not match + + )} +
+ )} +
+ +
+ + {/*
+
+ Set status as your default wallet to ensure seamless dApp + connections +
+ + form.setValue('isDefaultWallet', value) + } + /> +
*/} + +
+
+
+
+ ) +} + +export { CreatePasswordForm } +export type { FormValues as CreatePasswordFormValues } diff --git a/packages/wallet/src/components/index.tsx b/packages/wallet/src/components/index.tsx index 9e84d1bd7..6fd4c508f 100644 --- a/packages/wallet/src/components/index.tsx +++ b/packages/wallet/src/components/index.tsx @@ -3,6 +3,10 @@ export * from '../utils/variants' export { AccountMenu } from './account-menu' export { type Account, Address, type AddressProps } from './address' export { AssetsList } from './assets-list' +export { + CreatePasswordForm, + type CreatePasswordFormValues, +} from './create-password-form' export { CurrencyAmount } from './currency-amount' export { DeleteAddressAlert } from './delete-address-alert' export { Image, type ImageProps } from './image' @@ -11,6 +15,7 @@ export { Navbar } from './nav-bar' export { NetworkExplorerLogo } from './network-explorer-logo' export { NetworkLogo } from './network-logo' export { PercentageChange } from './percentage-change' +export { PinExtension } from './pin-extension' export { SettingsPopover } from './settings-popover' export { ShortenAddress, diff --git a/packages/wallet/src/components/nav-bar/index.tsx b/packages/wallet/src/components/nav-bar/index.tsx index 4fd87c6ac..eaa4339a8 100644 --- a/packages/wallet/src/components/nav-bar/index.tsx +++ b/packages/wallet/src/components/nav-bar/index.tsx @@ -2,17 +2,8 @@ // import { ConnectButton } from '../connect-button' import { Logo } from '../logo' -import { SettingsPopover } from '../settings-popover' - -type Props = { - pathname: string -} - -const Navbar = (props: Props) => { - const { pathname } = props - - const isRoot = pathname === '-/' +const Navbar = () => { return (
{
- - {!isRoot && ( -
- {/* */} - -
- )}
) } diff --git a/packages/wallet/src/components/password-input/index.tsx b/packages/wallet/src/components/password-input/index.tsx new file mode 100644 index 000000000..bc2bb3231 --- /dev/null +++ b/packages/wallet/src/components/password-input/index.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useState } from 'react' + +import { HideIcon, RevealIcon } from '@status-im/icons/20' +import { cx } from 'class-variance-authority' +import { useController } from 'react-hook-form' + +import type { UseControllerProps } from 'react-hook-form' + +export const MIN_PASSWORD_LENGTH = 12 +export const MAX_PASSWORD_LENGTH = 128 + +type Props = React.ComponentPropsWithoutRef<'input'> & + UseControllerProps & { + label: string + placeholder?: string + } + +const PasswordInput = (props: Props) => { + const { label, placeholder, ...inputProps } = props + const [showPassword, setShowPassword] = useState(false) + + const { field, fieldState } = useController(props) + const invalid = fieldState.invalid + + return ( +
+ + +
+ ) +} + +export { PasswordInput } diff --git a/packages/wallet/src/components/password-strength/hooks/use-password-strength.tsx b/packages/wallet/src/components/password-strength/hooks/use-password-strength.tsx new file mode 100644 index 000000000..f81988b88 --- /dev/null +++ b/packages/wallet/src/components/password-strength/hooks/use-password-strength.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react' + +import { zxcvbn } from '@zxcvbn-ts/core' + +const STRENGTH_LEVELS = { + 0: { + label: 'Very weak', + color: 'text-danger-50', + }, + 1: { + label: 'Weak', + color: 'text-customisation-orange-60', + }, + 2: { + label: 'Okay', + color: 'text-customisation-yellow-50', + }, + 3: { + label: 'Strong', + color: 'text-success-50', + }, + 4: { + label: 'Very strong', + color: 'text-success-50', + }, +} as const + +type StrengthScore = keyof typeof STRENGTH_LEVELS + +export const usePasswordStrength = (password: string) => { + const defaultStrength = STRENGTH_LEVELS[0] + + return useMemo(() => { + const requirements = [ + { + label: 'Lowercase', + rule: /[a-z]/.test(password), + }, + { + label: 'Uppercase', + rule: /[A-Z]/.test(password), + }, + { + label: 'Number', + rule: /[0-9]/.test(password), + }, + { + label: 'Symbol', + rule: /[^a-zA-Z0-9]/.test(password), + }, + ] + + const result = password ? zxcvbn(password) : { score: 0 } + const score = result.score as StrengthScore + const strengthLevel = STRENGTH_LEVELS[score] || defaultStrength + + return { + requirements, + score: result.score, + label: strengthLevel.label, + color: strengthLevel.color, + characterCount: password.length, + } + }, [password, defaultStrength]) +} diff --git a/packages/wallet/src/components/password-strength/index.tsx b/packages/wallet/src/components/password-strength/index.tsx new file mode 100644 index 000000000..fb63fa552 --- /dev/null +++ b/packages/wallet/src/components/password-strength/index.tsx @@ -0,0 +1,46 @@ +import { cx } from 'class-variance-authority' + +import { usePasswordStrength } from './hooks/use-password-strength' +import { PasswordStrengthIndicator } from './indicator' + +type Props = { + password: string +} + +const PasswordStrength = ({ password }: Props) => { + const { score, label, color, requirements } = usePasswordStrength(password) + + return ( +
+
+ {password.length <= 0 ? ( +
+ Tip: Include a mixture of numbers, capitals and symbols +
+ ) : ( +
+ + {label} +
+ )} +
+ +
+ {requirements.map((requirement, index) => ( +
+ + {requirement.label} + +
+ ))} +
+
+ ) +} + +export { PasswordStrength } diff --git a/packages/wallet/src/components/password-strength/indicator.tsx b/packages/wallet/src/components/password-strength/indicator.tsx new file mode 100644 index 000000000..0c70e495f --- /dev/null +++ b/packages/wallet/src/components/password-strength/indicator.tsx @@ -0,0 +1,39 @@ +export const PasswordStrengthIndicator = ({ + score, + size = 16, +}: { + score: number + size?: number +}) => { + const radius = (size - 1.2) / 2 + const circumference = 2 * Math.PI * radius + const strokeDashoffset = circumference * (1 - (score * 25) / 100) + const center = size / 2 + + return ( + + + + + + ) +} diff --git a/packages/wallet/src/components/pin-extension/index.tsx b/packages/wallet/src/components/pin-extension/index.tsx new file mode 100644 index 000000000..e2e4c6ef5 --- /dev/null +++ b/packages/wallet/src/components/pin-extension/index.tsx @@ -0,0 +1,47 @@ +import { Button } from '@status-im/components' +import { CloseIcon, PinIcon, PuzzleIcon } from '@status-im/icons/16' +import { cx } from 'class-variance-authority' + +import { Logo } from '../logo' + +type Props = { + onClose: () => void +} + +export const PinExtension = ({ onClose }: Props) => { + return ( +
+
+
+ +
+

Pin the Status extension

+
+ Click } /> then + } /> +
+
+
+ ) +} + +const FakeIconButton = ({ icon }: { icon: React.ReactNode }) => { + return ( +
+ {icon} +
+ ) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85edae373..19453cc6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,13 +95,13 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/next': specifier: 10.45.2 - version: 10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@trpc/server': specifier: 10.45.2 version: 10.45.2 next: specifier: 15.3.0 - version: 15.3.0(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + version: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: specifier: ^19.0.0 version: 19.1.0 @@ -852,6 +852,9 @@ importers: '@cardano-sdk/key-management': specifier: ^0.27.5 version: 0.27.9(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@hookform/resolvers': + specifier: ^3.1.1 + version: 3.10.0(react-hook-form@7.56.3(react@19.1.0)) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1303,6 +1306,9 @@ importers: packages/wallet: dependencies: + '@hookform/resolvers': + specifier: ^3.3.4 + version: 3.10.0(react-hook-form@7.57.0(react@18.3.1)) '@radix-ui/react-accordion': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1354,6 +1360,9 @@ importers: '@types/async-retry': specifier: ^1.4.9 version: 1.4.9 + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 async-retry: specifier: ^1.3.3 version: 1.3.3 @@ -1368,7 +1377,7 @@ importers: version: 2.30.0 next: specifier: 15.1.6 - version: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4) + version: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4) qrcode.react: specifier: ^3.1.0 version: 3.2.0(react@18.3.1) @@ -1378,6 +1387,9 @@ importers: react-day-picker: specifier: ^8.7.1 version: 8.7.1(date-fns@2.30.0)(react@18.3.1) + react-hook-form: + specifier: ^7.57.0 + version: 7.57.0(react@18.3.1) react-swipeable: specifier: ^7.0.1 version: 7.0.2(react@18.3.1) @@ -1394,6 +1406,9 @@ importers: specifier: ^4.3.7 version: 4.3.7(react@18.3.1) devDependencies: + '@hookform/devtools': + specifier: ^4.3.1 + version: 4.4.0(@types/react@19.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@status-im/eslint-config': specifier: workspace:* version: link:../eslint-config @@ -8689,6 +8704,9 @@ packages: '@wxt-dev/storage@1.1.1': resolution: {integrity: sha512-H1vYWeoWz03INV4r+sLYDFil88b3rgMMfgGp/EXy3bLbveJeiMiFs/G0bsBN2Ra87Iqlf2oVYRb/ABQpAugbew==} + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + '@zxing/text-encoding@0.9.0': resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} @@ -8998,6 +9016,7 @@ packages: auri@1.0.2: resolution: {integrity: sha512-OC54Bv+hAPvYlo98ZwK3cTo2ijg0CBylaAO5dZ/xZQ7e897k0o4qtxkUPQHruJrFUnL5BebCXz+bUAeXQHp9dg==} + deprecated: No longer supported hasBin: true auto-bind@4.0.0: @@ -11134,6 +11153,10 @@ packages: fast-url-parser@1.1.3: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -15176,6 +15199,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hook-form@7.57.0: + resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -17998,7 +18027,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/generator': 7.25.6 '@babel/parser': 7.25.6 - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 '@babel/traverse': 7.25.6(supports-color@5.5.0) '@babel/types': 7.25.6 babel-preset-fbjs: 3.4.0(@babel/core@7.25.2) @@ -19100,8 +19129,8 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.24.7(supports-color@5.5.0) - '@babel/runtime': 7.22.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.1 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -19138,9 +19167,25 @@ snapshots: '@emotion/memoize@0.9.0': {} + '@emotion/react@11.14.0(@types/react@19.1.0)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.1 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.0 + transitivePeerDependencies: + - supports-color + '@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0)': dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 @@ -19164,9 +19209,24 @@ snapshots: '@emotion/sheet@1.4.0': {} + '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@18.3.1))(@types/react@19.1.0)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.1 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@19.1.0)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.0 + transitivePeerDependencies: + - supports-color + '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)': dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 '@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.1.0) @@ -19185,6 +19245,10 @@ snapshots: '@emotion/unitless@0.7.5': {} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': dependencies: react: 19.1.0 @@ -20252,6 +20316,22 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@hookform/devtools@4.4.0(@types/react@19.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/react': 11.14.0(@types/react@19.1.0)(react@18.3.1) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@18.3.1))(@types/react@19.1.0)(react@18.3.1) + '@types/lodash': 4.14.191 + little-state-machine: 4.8.1(react@18.3.1) + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-simple-animate: 3.5.3(react-dom@18.3.1(react@18.3.1)) + use-deep-compare-effect: 1.8.1(react@18.3.1) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - supports-color + '@hookform/devtools@4.4.0(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.1.0) @@ -20272,6 +20352,10 @@ snapshots: dependencies: react-hook-form: 7.56.3(react@19.1.0) + '@hookform/resolvers@3.10.0(react-hook-form@7.57.0(react@18.3.1))': + dependencies: + react-hook-form: 7.57.0(react@18.3.1) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -26349,7 +26433,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@6.5.1': dependencies: - '@babel/types': 7.25.6 + '@babel/types': 7.27.1 entities: 4.4.0 '@svgr/hast-util-to-babel-ast@8.0.0': @@ -26669,7 +26753,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 '@types/aria-query': 5.0.1 aria-query: 5.3.0 chalk: 4.1.2 @@ -26781,13 +26865,13 @@ snapshots: '@trpc/server': 10.45.2 typescript: 5.8.3 - '@trpc/next@10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@trpc/next@10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/react-query': 5.75.5(react@19.1.0) '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/react-query': 11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) '@trpc/server': 10.45.2 - next: 15.3.0(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -28626,6 +28710,10 @@ snapshots: async-mutex: 0.5.0 dequal: 2.0.3 + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + '@zxing/text-encoding@0.9.0': optional: true @@ -29004,7 +29092,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -31600,6 +31688,8 @@ snapshots: dependencies: punycode: 1.4.1 + fastest-levenshtein@1.0.16: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -33259,7 +33349,7 @@ snapshots: json-schema-to-ts@2.9.2: dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 '@types/json-schema': 7.0.9 ts-algebra: 1.2.2 @@ -33663,6 +33753,10 @@ snapshots: lit-element: 4.2.0 lit-html: 3.3.0 + little-state-machine@4.8.1(react@18.3.1): + dependencies: + react: 18.3.1 + little-state-machine@4.8.1(react@19.1.0): dependencies: react: 19.1.0 @@ -35151,7 +35245,7 @@ snapshots: minimist: 1.2.8 next: 15.3.0(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) - next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4): + next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4): dependencies: '@next/env': 15.1.6 '@swc/counter': 0.1.3 @@ -35161,7 +35255,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.27.1)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 15.1.6 '@next/swc-darwin-x64': 15.1.6 @@ -35205,6 +35299,33 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4): + dependencies: + '@next/env': 15.3.0 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001660 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.27.1)(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.0 + '@next/swc-darwin-x64': 15.3.0 + '@next/swc-linux-arm64-gnu': 15.3.0 + '@next/swc-linux-arm64-musl': 15.3.0 + '@next/swc-linux-x64-gnu': 15.3.0 + '@next/swc-linux-x64-musl': 15.3.0 + '@next/swc-win32-arm64-msvc': 15.3.0 + '@next/swc-win32-x64-msvc': 15.3.0 + '@opentelemetry/api': 1.9.0 + sass: 1.80.4 + sharp: 0.34.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + nlcst-is-literal@2.1.1: dependencies: '@types/nlcst': 1.0.4 @@ -36592,6 +36713,10 @@ snapshots: dependencies: react: 19.1.0 + react-hook-form@7.57.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -36707,6 +36832,10 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + react-simple-animate@3.5.3(react-dom@18.3.1(react@18.3.1)): + dependencies: + react-dom: 18.3.1(react@18.3.1) + react-simple-animate@3.5.3(react-dom@19.1.0(react@19.1.0)): dependencies: react-dom: 19.1.0(react@19.1.0) @@ -37065,7 +37194,7 @@ snapshots: relay-runtime@12.0.0: dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 fbjs: 3.0.5 invariant: 2.2.4 transitivePeerDependencies: @@ -38899,10 +39028,19 @@ snapshots: optionalDependencies: '@babel/core': 7.25.2 - styled-jsx@5.1.6(react@18.3.1): + styled-jsx@5.1.6(@babel/core@7.27.1)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.27.1 + + styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + optionalDependencies: + '@babel/core': 7.27.1 stylis@4.2.0: {} @@ -39844,9 +39982,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + use-deep-compare-effect@1.8.1(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.1 + dequal: 2.0.3 + react: 18.3.1 + use-deep-compare-effect@1.8.1(react@19.1.0): dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.27.1 dequal: 2.0.3 react: 19.1.0