Skip to content

feat: passphrase support#292

Merged
dadamu merged 12 commits into
mainfrom
feat/passphrase
Jul 3, 2026
Merged

feat: passphrase support#292
dadamu merged 12 commits into
mainfrom
feat/passphrase

Conversation

@dadamu

@dadamu dadamu commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Pull Request Checklist

Before Submission


Description


Additional Notes


By submitting this pull request, I confirm that:

  • My contribution is made under the Business Source License
  • I grant Forbole Technology Limited a perpetual, worldwide, non-exclusive license to use my contribution under the
    project's
    license terms
  • I have the authority to make this contribution and license it appropriately

Summary by CodeRabbit

  • New Features
    • Added a wallet import flow for recovery phrase with optional passphrase, wired into onboarding and popup routing.
    • Introduced full-page screens/components for onboarding, password creation, passphrase/recovery-phrase import, and wallet selection/import.
    • Expanded Storybook setup and added UI stories (plus new reusable page components like headers and modals).
  • Bug Fixes
    • Passphrases are now consistently applied during mnemonic-based derivation and signer/account creation.
  • Refactor / UX
    • Refactored onboarding and add/import flows to delegate UI to dedicated full-page components; improved fullscreen layout sizing.

Copilot AI review requested due to automatic review settings July 2, 2026 09:44
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

.coderabbit.yml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized keys: "labels", "include_paths", "exclude_paths", "filters", "review", "pull_request", "limits", "commands", "messages"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

This PR adds Storybook support, shared page components, and a passphrase-aware recovery phrase import flow. It also threads passphrase data through wallet derivation and updates onboarding and popup routing to use the new screens.

Changes

Storybook and shared UI pages

Layer / File(s) Summary
Storybook setup
.storybook/main.ts, .storybook/preview.ts, package.json, tailwind.config.js, ui/globals.css
Adds Storybook configuration, scripts, dependencies, Tailwind scanning, and global stylesheet directives.
Shared page components
ui/general/PageHeader.tsx, ui/full-page/*Page.tsx, ui/popup/add-wallet/AddWalletPage.tsx, related .stories.tsx, components/layouts/FullscreenLayout.tsx
Adds reusable page components and stories, including header, welcome, password, import, and wallet selection pages.
Screen wiring and navigation
components/onboarding/Welcome.tsx, components/onboarding/SetupPassword.tsx, components/onboarding/ChooseImport.tsx, components/screens/AddWallet.tsx, lib/utils.ts
Replaces inline layouts with shared pages and introduces full-page navigation from the popup.
Fullscreen layout tweak
components/layouts/FullscreenLayout.tsx
Changes the layout wrapper height class from h-screen to min-h-screen.

Passphrase import flow

Layer / File(s) Summary
Passphrase derivation plumbing
types/WalletSecret.ts, lib/wallet/account-factory.ts, lib/ethereum/wallet/account-factory.ts, hooks/wallet/useWalletImporter.ts, lib/service/handlers/kaspa/utils.ts, lib/service/handlers/evm/utils.ts
Adds an optional passphrase field and passes it through mnemonic import, seed derivation, stored wallet secrets, and signer creation.
Passphrase UI and onboarding flow
ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx, ui/full-page/import-passphrase/ImportPassphrasePage.tsx, related .stories.tsx, components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx, components/screens/Onboarding.tsx, entrypoints/popup/router.tsx
Adds the recovery phrase and passphrase screens, the coordinating onboarding screen, and the new popup route.
Estimated code review effort: 4 (Complex) ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ImportRecoveryPhraseWithPassphrase
  participant ImportRecoveryPhrasePage
  participant ImportPassphrasePage
  participant useWalletImporter
  participant AccountFactory
  participant Keyring

  User->>ImportRecoveryPhrasePage: submit recovery words
  ImportRecoveryPhraseWithPassphrase->>ImportRecoveryPhraseWithPassphrase: validate mnemonic and store it
  ImportRecoveryPhraseWithPassphrase->>ImportPassphrasePage: render passphrase step
  User->>ImportPassphrasePage: submit passphrase
  ImportRecoveryPhraseWithPassphrase->>Keyring: initKeyring(password)
  ImportRecoveryPhraseWithPassphrase->>useWalletImporter: importWalletByMnemonic(id, mnemonic, passphrase)
  useWalletImporter->>AccountFactory: createFromMnemonic(mnemonic, index, passphrase)
  useWalletImporter->>Keyring: addWalletSecret(..., passphrase)
Loading

Possibly related PRs

  • forbole/kastle#236: Both PRs change the mnemonic account derivation call chain in lib/ethereum/wallet/account-factory.ts and its callers.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding passphrase support across wallet import and creation flows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/passphrase

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces UI building blocks and onboarding flows to support importing a wallet from a recovery phrase with an additional BIP-39 passphrase, while also adding Storybook setup for developing the new UI components.

Changes:

  • Add a new “Recovery phrase with passphrase” import flow (phrase step → passphrase step) and wire it into onboarding + popup routing.
  • Refactor onboarding/popup screens to use new reusable UI pages (Welcome, Create Password, Import Wallet, Add Wallet, Import Phrase/Passphrase).
  • Add Storybook + Tailwind UI scanning to support UI component development.

Reviewed changes

Copilot reviewed 33 out of 35 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ui/popup/add-wallet/AddWalletPage.tsx New popup “Add Wallet” UI page with standard + advanced options.
ui/popup/add-wallet/AddWalletPage.stories.tsx Storybook stories for the popup add-wallet page.
ui/globals.css Add Tailwind base/components/utilities for Storybook/UI pages.
ui/general/PageHeader.tsx New shared header component for full-page/popup pages.
ui/general/PageHeader.stories.tsx Storybook stories for PageHeader variants.
ui/full-page/welcome/WelcomePage.tsx New full-page welcome UI.
ui/full-page/welcome/WelcomePage.stories.tsx Storybook stories for WelcomePage.
ui/full-page/import-wallet/ImportWalletPage.tsx New full-page “import method chooser” UI including advanced methods.
ui/full-page/import-wallet/ImportWalletPage.stories.tsx Storybook stories for ImportWalletPage.
ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx New recovery phrase entry UI (12/24 words, paste-all, hide/show, modal).
ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.stories.tsx Storybook stories for ImportRecoveryPhrasePage.
ui/full-page/import-passphrase/ImportPassphrasePage.tsx New passphrase entry UI (hide/show + modal).
ui/full-page/import-passphrase/ImportPassphrasePage.stories.tsx Storybook stories for ImportPassphrasePage.
ui/full-page/create-password/CreatePasswordPage.tsx New create-password UI (strength meter + terms checkbox).
ui/full-page/create-password/CreatePasswordPage.stories.tsx Storybook stories for CreatePasswordPage.
types/WalletSecret.ts Extend wallet secret shape to optionally include a passphrase.
tailwind.config.js Include ui/**/*.{ts,tsx} in Tailwind content scanning.
package.json Add Storybook scripts and Storybook dev dependencies.
package-lock.json Lockfile updates for Storybook dependencies.
lib/wallet/account-factory.ts Add optional passphrase support to mnemonic-to-seed derivation (Kaspa wallets).
lib/utils.ts Add openFullPage() helper for opening fullscreen popup routes in a new tab.
lib/service/handlers/kaspa/utils.ts Pass stored passphrase into mnemonic signer derivation (Kaspa).
lib/service/handlers/evm/utils.ts Pass stored passphrase into mnemonic signer derivation (EVM).
lib/ethereum/wallet/account-factory.ts Add optional passphrase support to mnemonic-to-seed derivation (EVM wallets).
hooks/wallet/useWalletImporter.ts Store optional passphrase alongside mnemonic and use it for wallet derivation.
entrypoints/popup/router.tsx Add route for the new “import recovery phrase with passphrase” screen.
components/screens/Onboarding.tsx Add onboarding step for passphrase import flow.
components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx New two-step import screen (phrase → passphrase) wired into onboarding.
components/screens/AddWallet.tsx Replace inline popup UI with AddWalletPage + advanced passphrase option; open full-page routes via helper.
components/onboarding/Welcome.tsx Refactor onboarding welcome to use WelcomePage UI component.
components/onboarding/SetupPassword.tsx Refactor setup password screen to use CreatePasswordPage UI component.
components/onboarding/ChooseImport.tsx Refactor import-method chooser to use ImportWalletPage UI component (incl. advanced passphrase method).
components/layouts/FullscreenLayout.tsx Adjust layout to min-h-screen to better support tall content.
.storybook/preview.ts Storybook preview setup (icons, globals.css, custom viewports).
.storybook/main.ts Storybook Vite config + alias for @ imports.

Comment thread ui/general/PageHeader.tsx
Comment on lines +27 to +32
<button
className="flex size-[46px] items-center justify-center rounded-lg text-white hover:bg-daintree-800"
onClick={onBack}
>
<i className="hn hn-angle-left text-xl" />
</button>
Comment thread ui/general/PageHeader.tsx
Comment on lines +47 to +52
<button
className="flex size-[46px] items-center justify-center rounded-lg text-white hover:bg-daintree-800"
onClick={onClose}
>
<i className="hn hn-times text-xl" />
</button>
Comment on lines +57 to +60
{
label: "Recovery phrase",
description: "Use a 12- or 24-word recovery phrase, or a private key.",
},
Comment on lines +16 to +20
isLoading: { control: "boolean" },
error: { control: "text" },
onBack: { action: "back" },
onPassphraseInfo: { action: "passphraseInfo" },
onSubmit: { action: "submit" },
const [isHidden, setIsHidden] = useState(true);
const [showModal, setShowModal] = useState(false);

const disabled = !passphrase || isLoading;
Comment on lines +17 to +23
function PassphraseInfoModal({ onClose }: { onClose: () => void }) {
return (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/60 p-6">
<div className="relative w-full max-w-[390px] rounded-2xl bg-icy-blue-950 p-10">
<button
className="absolute right-2.5 top-2.5 flex size-[38px] items-center justify-center rounded-lg text-daintree-400 hover:bg-daintree-800"
onClick={onClose}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
ui/globals.css (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stylelint misconfigured for Tailwind directives.

Stylelint is parsing this file with SCSS syntax (scss/at-rule-no-unknown), flagging standard @tailwind directives as errors. This is a config gap, not a code issue — add @tailwind (and @apply, @layer, etc.) to the rule's ignoreAtRules or configure customSyntax: "postcss"/postcss-scss exceptions in .stylelintrc so Tailwind directives aren't misreported as errors in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/globals.css` around lines 1 - 3, Stylelint is treating Tailwind directives
in the globals stylesheet as invalid SCSS at-rules, so update the Stylelint
configuration rather than the CSS itself. In the Stylelint config that governs
ui/globals.css, either add Tailwind directives like `@tailwind`, `@apply`, and
`@layer` to the scss/at-rule-no-unknown ignoreAtRules list or switch the
stylesheet parsing setup to the appropriate PostCSS syntax so these directives
are recognized correctly. Focus the change on the Stylelint rule/configuration
used for globals.css.

Source: Linters/SAST tools

lib/utils.ts (1)

99-103: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Unhandled rejection on browser.tabs.create.

browser.tabs.create returns a promise that can reject; it's not awaited or caught here, so failures fail silently (or surface as an unhandled rejection).

🔧 Proposed fix
-export function openFullPage(path: `/${string}`) {
+export async function openFullPage(path: `/${string}`) {
   const url = new URL(browser.runtime.getURL("/popup.html"));
   url.hash = path;
-  browser.tabs.create({ url: url.toString() });
+  await browser.tabs.create({ url: url.toString() });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/utils.ts` around lines 99 - 103, The openFullPage helper ignores the
promise returned by browser.tabs.create, which can lead to unhandled rejections.
Update openFullPage to await the tabs creation or explicitly handle the returned
promise with try/catch or a catch handler, and make sure any failure from
browser.tabs.create is surfaced or logged appropriately while keeping the URL
construction logic in place.
ui/general/PageHeader.tsx (1)

26-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Icon-only buttons lack accessible names.

Back/close buttons render only an <i> icon with no aria-label, so screen readers announce nothing meaningful.

♿ Proposed fix
             <button
               className="flex size-[46px] items-center justify-center rounded-lg text-white hover:bg-daintree-800"
               onClick={onBack}
+              aria-label="Back"
             >

Also applies to: 46-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/general/PageHeader.tsx` around lines 26 - 33, The icon-only back/close
buttons in PageHeader have no accessible name, so add an explicit accessible
label to the button elements rendered by the showBack and close-button branches.
Update the relevant button markup in PageHeader so the button exposed by onBack
and the close action includes a meaningful aria-label (or equivalent accessible
text) while keeping the visual icon unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/onboarding/ChooseImport.tsx`:
- Around line 47-53: Guard the `onCreateWallet` flow in `ChooseImport.tsx`
against repeated clicks and failures: add a loading/disabled guard around the
`keyringInitialize` → `createNewWallet` → `emitWalletCreated` → `navigate`
sequence in the `onCreateWallet` handler, and wrap it in `try/catch/finally` so
rejected promises surface an error via `internalToast` and the loading state
resets. Use the same pattern already present in
`ImportRecoveryPhraseWithPassphrase.tsx`, and consider extracting this shared
wallet-creation logic for reuse with `SetupPassword.tsx` so both call sites
benefit from the same protection.

In `@components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx`:
- Around line 27-43: Move the mnemonic validation out of handlePassphraseSubmit
and into handlePhraseSubmit so invalid recovery phrases are caught immediately
after phrase entry. In ImportRecoveryPhraseWithPassphrase, validate with new
Mnemonic(words.join(" ")) before calling setStep("passphrase"), and only advance
to the passphrase step when the phrase is valid; keep the existing
invalid-phrase error handling tied to the phrase submission flow rather than the
passphrase submission flow.
- Around line 32-68: The retry path in handlePassphraseSubmit is re-running
keyringInitialize() even when the keyring is already set up, which causes
subsequent importWalletByMnemonic attempts to fail immediately. Update the
ImportRecoveryPhraseWithPassphrase flow to guard the
onboardingForm/keyringInitialize step so it only initializes once, or skip it
when the keyring is already initialized, then continue the import and navigation
logic as before.

In `@ui/full-page/create-password/CreatePasswordPage.tsx`:
- Around line 18-32: CreatePasswordPage’s submit flow currently calls onSubmit
without awaiting it, disabling, or handling rejections, which can allow
duplicate submissions and silent failures. Update the submit logic in
CreatePasswordPage to track an in-flight/loading state around onSubmit, disable
the submit button while the async handler is pending, and catch errors so they
can be surfaced through the existing error prop or local error state. Use the
CreatePasswordPage component and its submit button handler as the main
locations, and ensure callers like SetupPassword.tsx still work with an async
onSubmit.

In `@ui/full-page/import-passphrase/ImportPassphrasePage.tsx`:
- Around line 17-73: The PassphraseInfoModal component is duplicated in multiple
import pages and should be shared to prevent drift. Extract the existing
PassphraseInfoModal from ImportPassphrasePage into a reusable component under a
common location such as ui/general, then import and use it from both
ImportPassphrasePage and ImportRecoveryPhrasePage. Keep the public API the same
by preserving the onClose prop and the current modal behavior.

In `@ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx`:
- Around line 163-166: The visibility toggle icon mapping in
ImportRecoveryPhrasePage is inconsistent with ImportPassphrasePage, where
isHidden should correspond to the hidden-state icon and the opposite state to
the visible icon. Update the icon class logic in the toggle JSX so the symbol
selected for isHidden matches the convention used by the equivalent toggle in
ImportPassphrasePage, and keep the label text in sync with that state.
- Around line 102-111: The empty catch in handlePasteAll is tripping lint and
hides paste failures. Update the clipboard read/parse flow in
ImportRecoveryPhrasePage so the catch block is not empty: either log or surface
the error, and add user feedback when the pasted text does not contain 12 or 24
words. Keep the validation logic in handlePasteAll and the phrase state updates
(setPhraseLength, setWords) unchanged except for handling the invalid/failed
paste case.

---

Nitpick comments:
In `@lib/utils.ts`:
- Around line 99-103: The openFullPage helper ignores the promise returned by
browser.tabs.create, which can lead to unhandled rejections. Update openFullPage
to await the tabs creation or explicitly handle the returned promise with
try/catch or a catch handler, and make sure any failure from browser.tabs.create
is surfaced or logged appropriately while keeping the URL construction logic in
place.

In `@ui/general/PageHeader.tsx`:
- Around line 26-33: The icon-only back/close buttons in PageHeader have no
accessible name, so add an explicit accessible label to the button elements
rendered by the showBack and close-button branches. Update the relevant button
markup in PageHeader so the button exposed by onBack and the close action
includes a meaningful aria-label (or equivalent accessible text) while keeping
the visual icon unchanged.

In `@ui/globals.css`:
- Around line 1-3: Stylelint is treating Tailwind directives in the globals
stylesheet as invalid SCSS at-rules, so update the Stylelint configuration
rather than the CSS itself. In the Stylelint config that governs ui/globals.css,
either add Tailwind directives like `@tailwind`, `@apply`, and `@layer` to the
scss/at-rule-no-unknown ignoreAtRules list or switch the stylesheet parsing
setup to the appropriate PostCSS syntax so these directives are recognized
correctly. Focus the change on the Stylelint rule/configuration used for
globals.css.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5b90b695-3138-4079-9a6e-290e1897f241

📥 Commits

Reviewing files that changed from the base of the PR and between f673cf6 and 405ae75.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (34)
  • .storybook/main.ts
  • .storybook/preview.ts
  • components/layouts/FullscreenLayout.tsx
  • components/onboarding/ChooseImport.tsx
  • components/onboarding/SetupPassword.tsx
  • components/onboarding/Welcome.tsx
  • components/screens/AddWallet.tsx
  • components/screens/Onboarding.tsx
  • components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx
  • entrypoints/popup/router.tsx
  • hooks/wallet/useWalletImporter.ts
  • lib/ethereum/wallet/account-factory.ts
  • lib/service/handlers/evm/utils.ts
  • lib/service/handlers/kaspa/utils.ts
  • lib/utils.ts
  • lib/wallet/account-factory.ts
  • package.json
  • tailwind.config.js
  • types/WalletSecret.ts
  • ui/full-page/create-password/CreatePasswordPage.stories.tsx
  • ui/full-page/create-password/CreatePasswordPage.tsx
  • ui/full-page/import-passphrase/ImportPassphrasePage.stories.tsx
  • ui/full-page/import-passphrase/ImportPassphrasePage.tsx
  • ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.stories.tsx
  • ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx
  • ui/full-page/import-wallet/ImportWalletPage.stories.tsx
  • ui/full-page/import-wallet/ImportWalletPage.tsx
  • ui/full-page/welcome/WelcomePage.stories.tsx
  • ui/full-page/welcome/WelcomePage.tsx
  • ui/general/PageHeader.stories.tsx
  • ui/general/PageHeader.tsx
  • ui/globals.css
  • ui/popup/add-wallet/AddWalletPage.stories.tsx
  • ui/popup/add-wallet/AddWalletPage.tsx

Comment thread components/onboarding/ChooseImport.tsx
Comment thread components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx Outdated
Comment on lines +18 to +32
export interface CreatePasswordPageProps {
title?: string;
subtitle?: string;
passwordLabel?: string;
confirmPasswordLabel?: string;
passwordPlaceholder?: string;
confirmPasswordPlaceholder?: string;
hint?: string;
termsUrl?: string;
privacyUrl?: string;
buttonLabel?: string;
error?: string;
onBack?: () => void;
onSubmit?: (password: string) => void;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard async onSubmit with a loading state and error handling.

The submit button doesn't await or catch onSubmit, and isn't disabled while the callback is in flight. Consumers like SetupPassword.tsx pass an async handler that calls keyringInitialize/createNewWallet; a double-click can fire duplicate wallet-creation calls, and a rejected promise will fail silently with no user feedback.

🛠️ Proposed fix
 export interface CreatePasswordPageProps {
   title?: string;
   subtitle?: string;
   passwordLabel?: string;
   confirmPasswordLabel?: string;
   passwordPlaceholder?: string;
   confirmPasswordPlaceholder?: string;
   hint?: string;
   termsUrl?: string;
   privacyUrl?: string;
   buttonLabel?: string;
   error?: string;
   onBack?: () => void;
-  onSubmit?: (password: string) => void;
+  onSubmit?: (password: string) => void | Promise<void>;
 }

 export default function CreatePasswordPage({
   ...
 }: CreatePasswordPageProps) {
   const [password, setPassword] = useState("");
   const [confirm, setConfirm] = useState("");
   const [showPassword, setShowPassword] = useState(false);
   const [showConfirm, setShowConfirm] = useState(false);
   const [termsChecked, setTermsChecked] = useState(false);
+  const [submitting, setSubmitting] = useState(false);

   const strength = calcStrength(password);
   const mismatch = confirm.length > 0 && password !== confirm;
-  const disabled = !password || password !== confirm || !termsChecked;
+  const disabled =
+    !password || password !== confirm || !termsChecked || submitting;
             <button
               className={`...`}
               disabled={disabled}
-              onClick={() => !disabled && onSubmit?.(password)}
+              onClick={async () => {
+                if (disabled) return;
+                try {
+                  setSubmitting(true);
+                  await onSubmit?.(password);
+                } finally {
+                  setSubmitting(false);
+                }
+              }}
               type="button"
             >

Also applies to: 49-53, 190-201

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/full-page/create-password/CreatePasswordPage.tsx` around lines 18 - 32,
CreatePasswordPage’s submit flow currently calls onSubmit without awaiting it,
disabling, or handling rejections, which can allow duplicate submissions and
silent failures. Update the submit logic in CreatePasswordPage to track an
in-flight/loading state around onSubmit, disable the submit button while the
async handler is pending, and catch errors so they can be surfaced through the
existing error prop or local error state. Use the CreatePasswordPage component
and its submit button handler as the main locations, and ensure callers like
SetupPassword.tsx still work with an async onSubmit.

Comment thread ui/full-page/import-passphrase/ImportPassphrasePage.tsx Outdated
Comment thread ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx
Comment thread ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx
@dadamu dadamu changed the title Feat/passphrase feat: passphrase Jul 3, 2026
@dadamu dadamu changed the title feat: passphrase feat: passphrase support Jul 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx (1)

68-77: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Paste-all still fails silently on invalid input.

The empty-catch lint issue is fixed, but per the earlier review, pasting text that isn't 12/24 words (or a denied clipboard-read) still leaves the user with no feedback — nothing happens and no error is surfaced.

💡 Proposed fix
   const handlePasteAll = async () => {
     const text = await navigator.clipboard.readText().catch(() => null);
-    if (!text) return;
+    if (!text) {
+      onErrorClear?.();
+      return;
+    }
     const parsed = text.trim().split(/\s+/);
     if (parsed.length === 12 || parsed.length === 24) {
       onErrorClear?.();
       setPhraseLength(parsed.length as PhraseLength);
       setWords([...parsed, ...Array(24 - parsed.length).fill("")]);
+    } else {
+      // surface feedback, e.g. via a local toast or passed-in error setter
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx` around
lines 68 - 77, The handlePasteAll flow in ImportRecoveryPhrasePage still ignores
invalid pasted content and clipboard-read failures without telling the user.
Update the clipboard read and parsing path so that when
navigator.clipboard.readText fails or the pasted text is not 12/24 words, the
user gets a visible error via the existing onError/onErrorClear handling instead
of silently returning. Keep the success path in handlePasteAll unchanged for
valid phrases, but add a clear error message for the invalid/denied cases.
🧹 Nitpick comments (1)
ui/general/PassphraseInfoModal.tsx (1)

6-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add dialog semantics and keyboard support.

The overlay lacks role="dialog"/aria-modal, has no Escape-key handler, and the icon-only close button (Line 9-15) has no aria-label. Screen-reader and keyboard-only users have no accessible way to identify or dismiss the modal other than tabbing to the visible buttons.

♿ Proposed fix
-    <div className="absolute inset-0 z-10 flex items-center justify-center bg-black/60 p-6">
-      <div className="relative w-full max-w-[390px] rounded-2xl bg-icy-blue-950 p-10">
+    <div
+      className="absolute inset-0 z-10 flex items-center justify-center bg-black/60 p-6"
+      onKeyDown={(e) => e.key === "Escape" && onClose()}
+    >
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-label="What is a passphrase?"
+        className="relative w-full max-w-[390px] rounded-2xl bg-icy-blue-950 p-10"
+      >
         <button
           className="absolute right-2.5 top-2.5 flex size-[38px] items-center justify-center rounded-lg text-daintree-400 hover:bg-daintree-800"
           onClick={onClose}
+          aria-label="Close"
           type="button"
         >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/general/PassphraseInfoModal.tsx` around lines 6 - 16, The
PassphraseInfoModal overlay needs proper dialog accessibility and keyboard
dismissal. Update the modal container in PassphraseInfoModal to use dialog
semantics such as role="dialog" and aria-modal, add an Escape-key handler to
close it, and give the icon-only close button an explicit aria-label. Keep using
the existing onClose handler so both keyboard and screen-reader users can
reliably dismiss the modal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx`:
- Line 22: The keyring initialization guard in
ImportRecoveryPhraseWithPassphrase is only component-local, so remounting the
screen resets it and can call keyringInitialize() again after Keyring is already
initialized. Update the initialization flow in this screen to check
getKeyringStatus().isInitialized before invoking keyringInitialize(), or handle
the “Keyring is already initialized” error as a harmless no-op, and keep the
logic tied to the existing keyringReady/ref-based flow.

---

Duplicate comments:
In `@ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx`:
- Around line 68-77: The handlePasteAll flow in ImportRecoveryPhrasePage still
ignores invalid pasted content and clipboard-read failures without telling the
user. Update the clipboard read and parsing path so that when
navigator.clipboard.readText fails or the pasted text is not 12/24 words, the
user gets a visible error via the existing onError/onErrorClear handling instead
of silently returning. Keep the success path in handlePasteAll unchanged for
valid phrases, but add a clear error message for the invalid/denied cases.

---

Nitpick comments:
In `@ui/general/PassphraseInfoModal.tsx`:
- Around line 6-16: The PassphraseInfoModal overlay needs proper dialog
accessibility and keyboard dismissal. Update the modal container in
PassphraseInfoModal to use dialog semantics such as role="dialog" and
aria-modal, add an Escape-key handler to close it, and give the icon-only close
button an explicit aria-label. Keep using the existing onClose handler so both
keyboard and screen-reader users can reliably dismiss the modal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: aa9b5b84-3e1d-43e8-9fc6-4a0f952066f8

📥 Commits

Reviewing files that changed from the base of the PR and between 2cfefb3 and 380d22f.

📒 Files selected for processing (8)
  • components/onboarding/ChooseImport.tsx
  • components/onboarding/SetupPassword.tsx
  • components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx
  • ui/full-page/create-password/CreatePasswordPage.tsx
  • ui/full-page/import-passphrase/ImportPassphrasePage.tsx
  • ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.stories.tsx
  • ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.tsx
  • ui/general/PassphraseInfoModal.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • ui/full-page/import-recovery-phrase/ImportRecoveryPhrasePage.stories.tsx
  • ui/full-page/import-passphrase/ImportPassphrasePage.tsx
  • components/onboarding/ChooseImport.tsx
  • components/onboarding/SetupPassword.tsx
  • ui/full-page/create-password/CreatePasswordPage.tsx

const { importWalletByMnemonic } = useWalletImporter();
const onboardingForm = useFormContext<OnboardingData>();

const keyringReady = useRef(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file outline ==\n'
ast-grep outline components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx --view expanded || true

printf '\n== relevant file excerpt ==\n'
cat -n components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx | sed -n '1,220p'

printf '\n== keyring references ==\n'
rg -n "keyringInitialize|useKeyring|initialize\(|already initialized|setUp|isInitialized|keyringReady" components lib hooks -S || true

printf '\n== onboarding step flow references ==\n'
rg -n 'setValue\("step"|step"\s*:\s*"choose"|step\s*===\s*"choose"|onboardingForm' components -S || true

Repository: forbole/kastle

Length of output: 15166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== onboarding screen ==\n'
cat -n components/screens/Onboarding.tsx | sed -n '1,220p'

printf '\n== choose import screen ==\n'
cat -n components/onboarding/ChooseImport.tsx | sed -n '1,220p'

printf '\n== useKeyring hook ==\n'
cat -n hooks/useKeyring.ts | sed -n '1,220p'

printf '\n== keyring initialize handler ==\n'
cat -n lib/service/handlers/keyring-initialize.ts | sed -n '1,120p'

printf '\n== keyring manager initialize ==\n'
cat -n lib/keyring-manager.ts | sed -n '1,120p'

Repository: forbole/kastle

Length of output: 14331


Avoid re-initializing the keyring after this screen remounts. Around lines 47-49, keyringReady is component-local, so backing out to step="choose" unmounts this screen and resets the ref. Re-entering the flow will call keyringInitialize() again even if the keyring already exists, and Keyring.initialize() throws "Keyring is already initialized". Check getKeyringStatus().isInitialized before initializing, or treat that error as a no-op.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/screens/full-pages/ImportRecoveryPhraseWithPassphrase.tsx` at line
22, The keyring initialization guard in ImportRecoveryPhraseWithPassphrase is
only component-local, so remounting the screen resets it and can call
keyringInitialize() again after Keyring is already initialized. Update the
initialization flow in this screen to check getKeyringStatus().isInitialized
before invoking keyringInitialize(), or handle the “Keyring is already
initialized” error as a harmless no-op, and keep the logic tied to the existing
keyringReady/ref-based flow.

@dadamu dadamu merged commit ab9b50a into main Jul 3, 2026
2 checks passed
@dadamu dadamu deleted the feat/passphrase branch July 3, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants