| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- "use client";
- /**
- * Restore mavi from an existing recovery phrase (12 or 24 words).
- */
- import {
- countWords,
- isValidMnemonic,
- MAVI_MIN_PASSWORD_LENGTH,
- normalizeMnemonic,
- walletFromMnemonic,
- } from "@krypco/mavi-core";
- import Link from "next/link";
- import { useRouter } from "next/navigation";
- import { useState } from "react";
- import { PasswordInput } from "@/components/ui/password-input";
- import { MaviWalletLogo } from "@/components/wallet/wallet-logos";
- import { Button, buttonVariants } from "@/components/ui/button";
- import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- } from "@/components/ui/card";
- import { ErrorBanner } from "@/components/ui/error-banner";
- import {
- hasVault,
- saveNewVault,
- shortAddress,
- unlockVault,
- } from "@/lib/mavi/storage";
- import { writeLastLoginMethod } from "@/lib/last-login";
- import { cn } from "@/lib/utils";
- type Step = "phrase" | "password" | "done";
- export function ImportWalletFlow() {
- const router = useRouter();
- const [step, setStep] = useState<Step>("phrase");
- const [phrase, setPhrase] = useState("");
- const [password, setPassword] = useState("");
- const [password2, setPassword2] = useState("");
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const [address, setAddress] = useState<string | null>(null);
- const [wordCount, setWordCount] = useState<12 | 24 | null>(null);
- const wordsN = countWords(phrase);
- const phraseOk =
- (wordsN === 12 || wordsN === 24) && isValidMnemonic(phrase);
- function onPhraseContinue() {
- setError(null);
- const n = countWords(phrase);
- if (n !== 12 && n !== 24) {
- setError("Enter 12 or 24 recovery words (spaces between them).");
- return;
- }
- if (!isValidMnemonic(phrase)) {
- setError(
- "That phrase is not valid. Check spelling and order, then try again.",
- );
- return;
- }
- setWordCount(n === 24 ? 24 : 12);
- setStep("password");
- }
- async function onImport() {
- setError(null);
- if (password.length < MAVI_MIN_PASSWORD_LENGTH) {
- setError(
- `Password must be at least ${MAVI_MIN_PASSWORD_LENGTH} characters.`,
- );
- return;
- }
- if (password !== password2) {
- setError("Passwords do not match.");
- return;
- }
- if (hasVault()) {
- const ok = window.confirm(
- "A mavi wallet already exists on this browser. Replace it with the imported one? You will need the old recovery phrase to use the old wallet again.",
- );
- if (!ok) return;
- }
- setBusy(true);
- try {
- const wallet = walletFromMnemonic(normalizeMnemonic(phrase));
- await saveNewVault(password, wallet);
- await unlockVault(password);
- writeLastLoginMethod("mavi");
- setAddress(wallet.address);
- setStep("done");
- } catch (e) {
- setError(e instanceof Error ? e.message : "Import failed.");
- } finally {
- setBusy(false);
- }
- }
- return (
- <div className="mx-auto max-w-xl space-y-4">
- {error ? (
- <ErrorBanner title="Could not import">{error}</ErrorBanner>
- ) : null}
- {step === "phrase" ? (
- <Card>
- <CardHeader>
- <div className="mb-2 flex items-center gap-2">
- <MaviWalletLogo size={28} />
- <CardTitle>Import mavi wallet</CardTitle>
- </div>
- <CardDescription>
- Paste or type your secret recovery phrase. Words never leave this
- browser. Works with 12 or 24 words.
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="space-y-2">
- <label htmlFor="mavi-import-phrase" className="text-sm font-medium">
- Recovery phrase
- </label>
- <textarea
- id="mavi-import-phrase"
- rows={4}
- value={phrase}
- onChange={(e) => setPhrase(e.target.value)}
- spellCheck={false}
- autoCapitalize="none"
- autoCorrect="off"
- className="flex w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
- placeholder="word1 word2 word3 …"
- />
- <p className="text-xs text-muted-foreground">
- {wordsN > 0
- ? `${wordsN} word${wordsN === 1 ? "" : "s"} detected${phraseOk ? " · looks valid" : ""}`
- : "Type or paste all words in order."}
- </p>
- </div>
- <Button
- type="button"
- className="w-full"
- disabled={!phraseOk}
- onClick={onPhraseContinue}
- >
- Continue
- </Button>
- <Link
- href="/portal/mavi/create"
- className="block text-center text-sm text-muted-foreground hover:text-foreground"
- >
- Or create a new mavi wallet
- </Link>
- </CardContent>
- </Card>
- ) : null}
- {step === "password" ? (
- <Card>
- <CardHeader>
- <CardTitle>Lock imported wallet</CardTitle>
- <CardDescription>
- Choose a password for this browser
- {wordCount ? ` · ${wordCount}-word phrase` : ""}.
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <PasswordInput
- id="mavi-import-pw"
- label="Password"
- value={password}
- onChange={setPassword}
- autoComplete="new-password"
- placeholder={`At least ${MAVI_MIN_PASSWORD_LENGTH} characters`}
- />
- <PasswordInput
- id="mavi-import-pw2"
- label="Confirm password"
- value={password2}
- onChange={setPassword2}
- autoComplete="new-password"
- placeholder="Type it again"
- onEnter={() => void onImport()}
- />
- <div className="flex flex-col gap-2 sm:flex-row">
- <Button
- type="button"
- variant="outline"
- className="sm:flex-1"
- disabled={busy}
- onClick={() => {
- setError(null);
- setStep("phrase");
- }}
- >
- Back
- </Button>
- <Button
- type="button"
- className="sm:flex-1"
- disabled={
- busy ||
- password.length < MAVI_MIN_PASSWORD_LENGTH ||
- password !== password2
- }
- onClick={() => void onImport()}
- >
- {busy ? "Importing…" : "Import mavi wallet"}
- </Button>
- </div>
- </CardContent>
- </Card>
- ) : null}
- {step === "done" && address ? (
- <Card>
- <CardHeader>
- <div className="mb-2 flex items-center gap-2">
- <MaviWalletLogo size={28} />
- <CardTitle>Import complete</CardTitle>
- </div>
- <CardDescription>
- Your mavi wallet is restored and unlocked for this tab.
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="rounded-xl border border-proof/30 bg-proof/5 px-4 py-3">
- <p className="text-xs uppercase tracking-wide text-muted-foreground">
- Address
- </p>
- <p className="mt-1 break-all font-mono text-sm">{address}</p>
- <p className="mt-1 text-xs text-muted-foreground">
- {shortAddress(address)}
- </p>
- </div>
- <Link
- href="/portal/mavi"
- className={cn(
- buttonVariants({ variant: "primary" }),
- "inline-flex w-full items-center justify-center gap-2",
- )}
- >
- <MaviWalletLogo size={20} />
- mavi wallet
- </Link>
- <Button
- type="button"
- variant="outline"
- className="w-full"
- onClick={() => router.push("/portal/dashboard")}
- >
- Go to dashboard
- </Button>
- </CardContent>
- </Card>
- ) : null}
- </div>
- );
- }
|