feat(email-recovery): UX overhaul matching design-doc §8.6#3857
Closed
sea-snake wants to merge 22 commits into
Closed
feat(email-recovery): UX overhaul matching design-doc §8.6#3857sea-snake wants to merge 22 commits into
sea-snake wants to merge 22 commits into
Conversation
This was referenced May 7, 2026
Closed
sea-snake
added a commit
to sea-snake/internet-identity
that referenced
this pull request
May 7, 2026
1abff24 to
10e979e
Compare
Brings the email-recovery flow up to the polish level of the rest of
the (new-styling) app. Driven by the staging-test feedback:
Wizard flow
- Add a 3-step progress indicator at the top of every wizard view
(`<Steps total={3} current={…}>` from createRecoveryPhrase).
- Replace `SendMagicEmail`'s verbose paragraph + icon-only copy + bottom
cancel + orange warning block with the mockup's pre-filled email card:
To / From / Subject / Body rows, whole-row copy buttons mirroring
ContinueOnNewDevice's tooltip pattern (`navigator.clipboard` →
`copied = true` → `waitFor(700)` → reset), a primary "Open in mail
app" mailto button, and a small "Waiting for your email…" indicator.
- Drop Cancel buttons from every wizard view; the Dialog's built-in ×
closes the flow, and on close the wizard's `onDestroy` flips
`polling = false` so the closure stops ticking after the unmount.
- Single full-width primary CTA on EnterAddress, Done, FailedView, and
EnterAddressForRecovery — matching the createRecoveryPhrase style.
Unsupported-domain handling
- New `UnsupportedDomain` view shown when `prepare_add` /
`prepare_delegation` returns `DomainNotAllowlisted` /
`DomainNotSupported`. Headed "Can't use this email", with a
collapsible `<details>` explaining why (no DNSSEC at apex, not on
allowlist) and what the user can do to fix it (enable DNSSEC at
registrar, or pick a known provider). Single "Try a different
address" button routes back to the address-entry view.
Recovery-methods tab + cards
- Rename the access-and-recovery layout's tab from "Recovery phrase"
to "Recovery methods" (the tab now covers both the phrase and the
email entry). E2e fixtures use URL navigation, so no surgery needed.
- Add a count badge to the tab: 1 if either phrase/email present, 2 if
both, hidden when 0 (matching the access-methods tab pattern).
- Page heading on /manage/recovery follows: "Recovery methods" with
copy that covers both methods.
- Switch the cards' wrapper from the single-column flex stack to a
`grid-cols-1 sm:grid-cols-2` 2-up grid. Cards stretch to row height
via grid's default `align-items: stretch`. On mobile they stack
vertically as before. Cards' internal @container queries already
handle the narrower-cell case (icon + heading + button stack
vertically when the cell is below ~32rem).
Remove-email confirmation dialog
- New `RemoveEmailRecovery.svelte` mirroring `RemovePasskey.svelte`:
warning FeaturedIcon + "Are you sure?" heading + two short
explanatory paragraphs + stacked danger primary "Remove email" and
tertiary "Cancel" buttons, with `ProgressRing` during the async
remove. Wrap the existing `handleRemoveEmail` call in a Dialog/state
pattern (`removingEmailRecovery: boolean`, `<Dialog>` block at the
bottom of the page) — currently the email was removed silently with
no user warning at all.
Out of scope
- No canister changes. No new tests; the existing `emailRecovery.spec.ts`
fixture still covers the prepare → send → terminal happy path. The
unsupported-domain view is not yet covered by an e2e test.
CI failed because the new SendConfirmationEmail.svelte and RemoveEmailRecovery.svelte imported the `Button` component, which was removed in upstream main (commit f859c0a) — the canonical replacement is plain `<button>` / `<a>` with `btn` / `btn-primary` / `btn-lg` / `btn-danger` / `btn-tertiary` / `btn-secondary` classes. Reshape both files to that pattern (matching the post-removal RemovePasskey and ContinueOnNewDevice implementations). Also rename `SendMagicEmail.svelte` → `SendConfirmationEmail.svelte` and update its two import sites — "magic" wasn't a term I should have introduced into the codebase.
Two changes match what the new wizard renders: - Wizard views no longer carry a Cancel button — close the dialog via the Dialog's built-in × (`aria-label="Close"`) instead. - Step 2 heading is "Send your confirmation email" (the previous "Send the magic email" string was removed along with the "magic" language). Updates the wizard-surface specs in `routes/emailRecovery.spec.ts` and the `expectMagicEmailView` / `cancel()` helpers in `fixtures/emailRecovery.ts` (renamed to `expectSendConfirmationEmailView` / `close()` to keep the fixture API honest about what it does).
…adge
Round of feedback from staging A:
- Recovery cards (4 components): action buttons moved to a row below
the icon+title+subtitle, left-aligned. InactiveRecoveryPhrase drops
the red error palette and matches the neutral grey of
InactiveEmailRecovery — both inactive cards read as "not yet
configured" rather than "something is wrong". Active versions
aligned to the same icon-and-text shape so the two cards in the
recovery-methods grid look consistent.
- Wizard views: removed the "ADD EMAIL RECOVERY — STEP N OF M"
uppercase label and dropped the `<Steps>` progress bar entirely
(flow is now effectively two steps; the indicator was chrome).
- Done.svelte deleted. On RegistrationSucceeded the wizard fires a
new `onSuccess(address)` callback; the host shows a toast
("Recovery email added — <address> is now a recovery method.") and
closes the dialog — same shape as the recovery-phrase activation
toast.
- Email-address inputs (setup + recovery) suppress browser
autocomplete + autocorrect + autocapitalize + 1Password / LastPass
overlays via the matching attributes.
- Continue button in busy state now renders a `<ProgressRing />`
alongside the "Verifying…" label, matching RemovePasskey's pattern.
- SendConfirmationEmail (renamed from SendMagicEmail in the prior
pass) heavily reshaped:
* heading → "Verify your email" (was the longer "Send your
confirmation email")
* description → "Send the email below. We'll verify it
automatically." (waiting state communicated by the description
itself; the spinner + expiration footer are gone)
* row layout: labels (To/From/Subject/Body) sit ABOVE their
values, not beside, so the dialog stays narrow
* To and Subject rows are whole-row click targets that copy to
the clipboard with the existing Tooltip/manual pattern; copy
icon trails. From and Body rows are plain text.
* "Open in mail app" button uses ExternalLinkIcon (the
universal "this opens outside the app" signal).
* From row carries a right-aligned shield-check "Verified" badge
with a hover tooltip that explains the trust story:
- DNSSEC path → "Your provider's DKIM key is verified
end-to-end via DNSSEC — no trusted intermediaries."
- DoH path → "Your provider's DKIM key is verified by
independent DNS resolvers — three out of five must agree."
Path is derived in the wizard glue from whether the FE-built
DNSSEC skeleton bundle was non-empty, then plumbed through the
sending stage.
- /recovery sign-in page: the "Recover with email" CTA is now a
primary button (was secondary), matching the prominence of
"Recover with phrase".
- E2E tests + fixture: heading-name expectations updated for "Verify
your email"; the post-success path no longer expects a Done view +
"Done" button — instead asserts the dialog closes and the bound
address is visible on the active card.
User-facing copy in three places: - Verification badge tooltip on the From row of SendConfirmationEmail: rephrased into two sentences (no em dash) and dropped the "three of five" specificity in favour of "quorum consensus across independent DNS resolvers" — the exact threshold is an implementation detail and could shift later. - The body row's "(anything — leave it blank)" is now "(anything, leave it blank)". - The UnsupportedDomain view loses two em dashes in its body copy in favour of two-sentence breaks.
- Removes `src/frontend/src/lib/components/ui/Button.svelte`, which carried a `// TODO: Deprecate this component, use classes directly on <button> and <a>` comment - Migrates all 36 callsites to plain `<button>` / `<a>` elements with the existing `btn-*` utility classes from `src/frontend/src/lib/styles/button.css` - One bundled refactor — no behavioral changes intended | Button prop | Replacement class | | --- | --- | | `variant="primary" \| "secondary" \| "tertiary"` | `btn-primary` / `btn-secondary` / `btn-tertiary` | | `size="sm" \| "md" \| "lg" \| "xl"` | `btn-sm` / `btn-md` / `btn-lg` / `btn-xl` | | `iconOnly` | `btn-icon` | | `danger` | `btn-danger` | | `href` present | rendered as `<a>` (otherwise `<button>`) | - [x] `npx svelte-check check` — 0 errors (19 pre-existing warnings, unchanged) - [x] `npx tsc --project ./tsconfig.all.json --noEmit` — passes - [x] `npm run lint:eslint` — passes - [x] `npm run format-check` — passes - [x] `npx vite build` — succeeds - [ ] Manual visual smoke through auth, manage, authorize flows Note: `src/try-ii/src/try-ii-frontend/src/lib/components/Button.svelte` is a separate, hand-rolled component in the `try-ii` sub-project with no deprecation marker — left untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
dfinity#3835) ## Problem NitroKey 3A passkeys fail to authenticate with Internet Identity with: > Invalid delegation: Invalid public key: Algorithm Unspecified not supported: Algorithm not supported in COSE parser The root cause is suspected to be extra COSE map entries (specifically `key_ops`, label 4) included in the public key when II creates discoverable credentials. These fields cause the IC's COSE parser to reject the key. To confirm the root cause, we need to capture the exact COSE bytes produced by the NitroKey 3A when using II's exact credential creation options, and attempt an actual IC canister call to reproduce the error. ## Changes ### `discoverablePasskeyIdentity.ts` - Fixes `authDataToCose` to use an **explicit allowlist** of the 5 standard COSE key parameters (`kty=1`, `alg=3`, `crv=-1`, `x=-2`, `y=-3`) instead of keeping all numeric keys. Previously label `4` (`key_ops`) would have been kept, which the IC rejects. - Exports new `authDataToCoseDebug` function that returns all COSE map entries (with hex-encoded values), filtered entries, raw COSE hex, and cleaned COSE hex for diagnostic purposes. ### Backend: new `whoami` canister method - Adds a trivial query method that returns the caller's principal - Registered in the DID file and generated frontend bindings - Exists purely to give the debug test a real canister endpoint to call ### Self-service: IC call test - After creating the passkey credential, the test prompts a second WebAuthn interaction to create a delegation chain and calls `whoami` on the canister - On success: captures `callerPrincipal` (confirms the passkey COSE key is IC-valid) - On failure: captures the exact IC error string (e.g. "Algorithm Unspecified not supported") plus `senderPubkeyHex` and `delegationPubkeyHex` - All captured in the JSON export for offline analysis ### `self-service/+page.svelte` - Updates `testPasskeyCreation` to go through `DiscoverablePasskeyIdentity` directly (using the same `creationOptions` and `sign()` path as normal II registration) via a custom `getPublicKey` callback that captures `authDataToCoseDebug` output. This ensures the test and real passkey creation code share the exact same credential creation path and can never diverge. - Captures `authDataToCoseDebug` output and DER-encoded key into the `debug` field of each test result. - This debug data (`credentialIdHex`, `rawCoseHex`, `cleanedCoseHex`, `derHex`, `allEntries`, `filteredEntries`, `icCall`) is included in the JSON export at `/self-service`. ## How to use The NitroKey 3A owner can go to `/self-service`, click "Test passkey support" (two WebAuthn prompts: create then sign), then "Export JSON" and share the file. The exported JSON will contain: - Raw COSE key bytes and all map entries (reveals any `key_ops` or extension fields) - DER-encoded key (exactly what gets sent to the IC) - IC call result: either the caller's principal (key is valid) or the exact error message ## Test plan - [ ] Visit `/self-service` and click "Test passkey support" with any passkey - [ ] Confirm two WebAuthn prompts appear (create + sign for delegation) - [ ] Click "Export JSON" and verify the exported file contains the `debug` field with `rawCoseHex`, `cleanedCoseHex`, `allEntries`, `filteredEntries`, `derHex`, `credentialIdHex`, and `icCall` - [ ] Verify `icCall` contains either `callerPrincipal` (success) or `error` (failure) - [ ] Verify normal passkey creation flow is unaffected --------- Co-authored-by: sea-snake <bot@sea-snake.dev> Co-authored-by: sea-snake-translation-bot <sea-snake-translation-bot@users.noreply.github.com> Co-authored-by: Claude <sandbox@claude.ai> Co-authored-by: sea-snake <sea-snake@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The success toast description includes the bound address as a
substring ("<addr> is now a recovery method."), which collided
with the active-card subtitle ("<addr>" alone) under
Playwright's default fuzzy-text matcher and tripped strict-mode.
Switch to { exact: true } to only match the card subtitle.
…cker - SendConfirmationEmail card: add `overflow-hidden` to the rounded container so the To/Subject row hover background respects the outer corners (was bleeding past the rounded corners). - Verification badge in the From row: drop the "Verified" text + background pill to avoid clashing with the dialog's "Verify your email" heading. Now an icon-only ShieldCheckIcon styled with `text-fg-success-primary` (renders correctly in both light and dark modes — the previous `bg-bg-success-secondary` pairing had poor contrast in dark). Tooltip carries the trust story on hover, matching the help-icon pattern from CreatePasskey (`direction="up" align="end" offset="0rem"`). Tooltip label is "Cryptographically authentic" so the hover content is distinct from the dialog heading's "Verify". - Subtitle shortened from "Send the email below. We'll verify it automatically." to "Send the email below to confirm." — fits one line on the dialog width. - /recovery picker: replace the two stacked solid buttons with two stacked `ButtonCard`s carrying a leading icon, label, descriptive subtitle, and a trailing chevron. Eliminates the heavy two-primary-buttons stack while keeping both options equally prominent. Phrase subtitle is "Type your 24 words." (II uses 24-word phrases); email subtitle is "Send a signed email from your inbox." — drops the word "token".
…rdance The help-icon Tooltip pattern in CreatePasskey wraps the icon in a `btn btn-tertiary btn-icon !rounded-full` button so it gets a circled hover/focus background and matches the rest of the toolbar icon language. Match exactly here so the verification shield reads as an interactive trust-info affordance. `!cursor-default` keeps the cursor at default so users don't expect a click action.
…ability
The /recovery page picker switched from buttons ("Recover with
phrase" / "Recover with email") to ButtonCards whose visible text
is now "Recovery phrase" / "Recovery email" plus a subtitle. The
existing e2e selectors (`getByRole('button', { name: 'Recover with
email' })`) computed the accessible name from the visible content
and stopped matching. Add explicit aria-labels so the buttons still
expose the same accessible name to screen readers and tests, no
fragile selector rewrites needed.
Three rounds of staging feedback rolled into one:
1. Verification badge alignment (SendConfirmationEmail). The shield
button used `btn-icon` + `size-5` icon (= 20×20 button via
aspect-square !p-0), while the To/Subject rows ended in plain
`size-4` CopyIcons (= 16×16). The 4 px size mismatch put the
shield's right edge to the left of the copy icons. Now the
button is `size-7!` (28×28) with a `size-4` shield inside; the
resulting 6 px padding around the icon is offset back with
`-me-1.5` so the icon's right edge lands at the same x as the
copy icons. Hover circle is now visible too.
2. Recovery-method cards rebuilt to match the access-method
`PasskeyItem` density. Each card has:
- icon row with the action affordance at the trailing edge
(single action → label button; multiple actions → kebab menu
via `Select`)
- bold heading (method name)
- small status subtitle ("Activated" / "Not activated" /
"Not verified")
- divider
- "Last used" stat (active variants only)
- description paragraph at the bottom
`ActiveRecoveryPhrase`, `InactiveRecoveryPhrase`,
`ActiveEmailRecovery`, `InactiveEmailRecovery`, and
`UnverifiedRecoveryPhrase` all converted. The active recovery-
email card moves the address back into the subtitle slot so the
heading stays "Recovery email" — same shape as the active
recovery-phrase card.
3. /recovery picker layout. Reflowed each ButtonCard into:
[icon | title | arrow]
subtitle
The icon and title share a flex row (icon centered against the
title text); the subtitle sits in a second row indented by
`ps-8` (icon size + gap-3) so it lines up with the title's left
edge. Static chevron replaced with the hover-fade `ArrowRightIcon`
pattern from the home-page identity picker — invisible at rest
(`opacity-0 me-3`), fades in and slides leftward
(`opacity-100 me-2`) on hover, slides further (`me-0`) on
keyboard focus.
ButtonCard renders as a <button> which inherits the user-agent `text-align: center` default. The icon+title+arrow row used flex alignment so it looked OK, but the standalone subtitle span inherited the centered default. Add `text-start` on the ButtonCard itself so all descendant text is start-aligned.
The card rebuild mirrored `PasskeyItem`'s use of <div> for the title,
but the e2e fixture queries `getByRole('heading', { name: 'Recovery
email' })`, which only matches semantic h1–h6 elements. Switching to
<h2> across all five recovery cards fixes the test and is the right
semantics anyway — these are section headings within the manage page.
- Both Inactive cards: btn-primary "Activate" with disambiguating
aria-labels ("Activate recovery phrase" / "Activate recovery email")
so the visible labels can match without breaking strict-mode test
selectors.
- ActiveEmailRecovery description: append "Use it to recover when you
lose all other access methods." anchor sentence to mirror the
recovery-phrase card and keep the two descriptions structurally
consistent.
- recovery/+page.svelte: hide the "How to stay secure" articles when
EMAIL_RECOVERY is on. The phrase-only safety tips read as awkward
filler once a second method shares the page; revisit if/when we
write multi-method tips worth the real estate.
- e2e: update fixture + spec selectors from "Add email" to
"Activate recovery email" to match the new aria-label.
The card structure was split into a heading ("Recovery phrase") plus a
separate status element ("Activated" / "Not activated" / "Not verified"),
so the old fixture queries for combined headings like "Recovery phrase
not activated" no longer match.
Scope each assertion to the section containing the "Recovery phrase"
heading, then check for the status text within it. This works whether
the email card is rendered alongside or not.
The recovery cards now share a parallel always-visible first sentence across active and inactive states, and the inactive cards add a second paragraph that fills the empty space below the divider (the active cards already have a "Last used" row in that slot). - Drop the trailing "Use it to recover when you lose all other access methods." sentence from all four cards. The cards live under the "Recovery methods" tab, so calling them recovery methods inside the cards too was redundant. - Phrase: replace "seed" with "phrase" — recovery phrases here can be reset, which conflicts with the cryptocurrency-world "seed" connotation of immutable provenance. - Inactive phrase paragraph: lean into the trust model (offline, no account or provider, only the user can use/lose/replace it). - Inactive email paragraph: explain that each email carries a cryptographic signature proving it came from the user's inbox; the only party they trust is their mail provider, with nothing else in between. (Avoid naming DNSSEC since the DoH-allowlist path doesn't rely on it.) - /recovery page picker: replace the phrase-only "Before getting started, find a private place and have your recovery phrase ready" blurb with a method-agnostic "Pick a recovery method below to sign back in".
Three small fixes uncovered by the e2e suite: - ManageRecoveryPage.verify() / .reset() now go through the "More options" dropdown on the unverified card. The earlier refactor consolidated those two actions into a Select menu, but the fixtures still expected directly visible buttons, leading to 60s timeouts on the entire `Recovery phrase > can be verified` describe. - CreateRecoveryPhraseWizard.close() targets the dialog X via its aria-label attribute. The role+name lookup matched both the X and the verify-step word button labelled "close" whenever the BIP39 wordlist happened to put "close" in the generated phrase, surfacing as a flaky strict-mode violation. - recovery/+page.svelte: cap the two-up card grid at max-w-5xl (~64rem / 1024px) so each card lands at roughly 500px wide on desktop instead of stretching to fill the full main column.
The trailing sentence echoed what the surrounding context already implied (the user just entered the unsupported domain), and isolating the domain on its own line made long ones wrap clumsily.
- Phrase option icon switches from KeyRound to Shield, matching the shield family used on every phrase card on /manage (Active/Inactive/ Unverified). The icon now consistently signals "recovery phrase" no matter which page the user is on. - Phrase subtitle: "Type your 24 words." -> "Type your 24 recovery words." — the prior phrasing was a bit short under the bold heading. - Email subtitle: "Send a signed email from your inbox." -> "Send an email from your inbox." — drop "signed" since it's irrelevant to the user-facing instruction at this picker step (the heading is enough).
Match the setup wizard's EnterAddress phrasing ("...to confirm.") so
the recover-flow subtitle reads consistently and fits on the same
number of lines without wrapping.
10e979e to
5b80360
Compare
Contributor
Author
|
Replaced with a new PR from an upstream branch (enables direct collaboration). Same content, new PR number. |
This was referenced May 12, 2026
Merged
pull Bot
pushed a commit
to mikeyhodl/internet-identity
that referenced
this pull request
May 12, 2026
…iring (dfinity#3838) ## Summary First PR in the email-recovery stack (`docs/ongoing/email-recovery.md` §10 Phase 0). Lands a working RFC-4035-compliant DNSSEC verifier for caller-supplied DNS proof bundles, plus the trust-anchor wiring that drives it. PR 2 (DKIM verifier) and PRs 4–9 (storage + recovery methods) build on this. ## What's in this PR ### Verifier core - New `dnssec/` module under `src/internet_identity/src/`: - `types.rs` — `DnsProofBundle`, `SignedRRset`, `DelegationLink`, `Rrsig`, `DnsName`, `DnssecError`, `VerifiedRecord`. - `canonical.rs` — owner-name canonicalization, RR canonical form, RRSIG signed-data construction (RFC 4034 §3.1.8.1, §6.2, §6.3), DS digest input. - `signature.rs` — algorithm dispatch + DS digest matching (SHA-256). - `verify.rs` — four-step algorithm (root anchor match → chain walk → leaf RRSIG → freshness). - Algorithm coverage (RFC 8624 MUST set): - **alg 8** — RSA-SHA256 (RFC 5702): root, com., most legacy zones. - **alg 13** — ECDSA-P256-SHA256 (RFC 6605): most TLDs, Cloudflare, modern zones. - **alg 15** — Ed25519 (RFC 8080): rare in production but mandatory. - Anything else returns `UnsupportedAlgorithm`. ### Wiring - New `DnssecConfig` and `DnssecRootAnchor` types in `internet_identity_interface`, exposed at the top of `InternetIdentityInit` as `dnssec_config: opt opt DnssecConfig` (set/clear semantics matching `analytics_config` and `dummy_auth`). - Trust-anchor list plumbed through `init`/`post_upgrade` into `PersistentState.dnssec_config` (and `StorablePersistentState` for cross-upgrade persistence). - `internet_identity.did` updated. ### Tests 13 unit tests in `dnssec/` covering: - Real cloudflare.com chain verifies end-to-end (exercises alg 8 at root and alg 13 at com → cloudflare.com → leaf). - Empty trust-anchor list rejected with `NoTrustAnchors`. - Wrong trust anchor rejected with `RootAnchorMismatch`. - Flipped byte in root DNSKEY → `RootAnchorMismatch` or `BadSignature`. - Flipped byte in leaf signature → `BadSignature`. - Stale signature (clock advanced past expiration) → `StaleOrFutureSignature`. - Unsupported algorithm (alg 5 / RSA-SHA1) → `UnsupportedAlgorithm(5)`. - Plus canonical-encoding + RFC 3110 RSA key parsing unit tests. ### Test infrastructure - `test_vectors/dnssec/cloudflare-com-2026-05.json` — real DoH-captured chain (root DNSKEY + 2 delegation links + leaf TXT). - `test_vectors/dnssec/iana-root-anchors-2026-05.json` — IANA root KSK trust anchors (Klajeyz/2017 + Kmyv6jo/2024). - `scripts/capture-dnssec-chain.py` — reproducible capture script using dnspython + DoH wire format. Tests use a frozen now from the capture's metadata so freshness checks stay stable indefinitely. ### New deps - `domain` (NLnet Labs, pure Rust) — referenced in docstrings for canonicalisation primitives; signature verification is hand-rolled on top of RustCrypto. - `p256` — ECDSA P-256 verification. - `ed25519-dalek` — Ed25519 verification. All three build cleanly for wasm32-unknown-unknown. ## What's deferred to later PRs in the stack - Captures for additional providers (proton.me, protonmail.com, tutanota.com — gmail.com / icloud.com / outlook.com / fastmail.com don't sign with DNSSEC; this is acknowledged in design doc §7.6). - Synthetic Ed25519 (alg 15) test vector — most production zones are alg 8 or 13; alg 15 is structurally exercised by the dispatch logic but doesn't have a real captured chain in this PR. ## Test plan - [x] `cargo check -p internet_identity --target wasm32-unknown-unknown` — clean (no warnings). - [x] `cargo test -p internet_identity --bin internet_identity` — 238 tests pass (was 227 pre-PR). - [x] `cargo test -p internet_identity_interface --lib` — 42 tests pass. - [x] `cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings` — clean. - [x] `cargo fmt --check` — clean (modulo a pre-existing diff in attributes.rs unrelated to this PR). - [x] CI on dfinity#3838 — fully green. ## Design doc https://github.com/sea-snake/internet-identity/blob/design/email-recovery/docs/ongoing/email-recovery.md (PR pending review on dfinity/internet-identity) ## Stack This is PR 1 of a 12-PR series. Subsequent PRs: - **PR 2** — mail-auth-backed DKIM verifier, consuming DnsProofBundle from this PR. - **PR 3** — DMARC alignment. - **PRs 4–9** — storage + Candid + behavior for email recovery. - **PRs 10–12** — frontend (DoH walker, Manage UI, recovery wizard). ## PR Stack | # | PR | Description | Status | |---|---|---|---| | 0 | [dfinity#3836](dfinity#3836) | Design doc | Open | | 1 | [dfinity#3838](dfinity#3838) | DNSSEC verifier scaffold | Open | | 2 | [dfinity#3839](dfinity#3839) | DKIM verifier (RFC 6376) | Open | | 3 | [dfinity#3840](dfinity#3840) | DMARC alignment (RFC 7489) | Open | | 4 | [dfinity#3841](dfinity#3841) | DoH fallback | Open | | 5+6 | [dfinity#3842](dfinity#3842) | Setup flow (storage + smtp_request) | Open | | 7 | [dfinity#3843](dfinity#3843) | Recovery flow (delegation) | Open | | 8 | [dfinity#3844](dfinity#3844) | Frontend + feature flag | Open | | 9 | [dfinity#3855](dfinity#3855) | Deploy/upgrade scripts: dnssec_config + doh_config | Open | | 10 | [dfinity#3857](dfinity#3857) | Email-recovery UX overhaul | Open |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Substantial polish round on the email-recovery FE matching feedback from staging-A testing. No canister changes; all four commits are FE-only.
mailto:"Open in mail app" button, subtle "Waiting for your email…" spinner. Cancel buttons removed across all wizard views — Dialog's × close is the only user-driven exit.UnsupportedDomainview shown when prepare returnsDomainNotAllowlisted/DomainNotSupported. Heads "Can't use this email", with a<details>collapsible explaining the technical reason and a how-to-fix pointer (enable DNSSEC at registrar, or pick a known provider).grid-cols-1 sm:grid-cols-2— 2-up tiles on desktop, stack on mobile. Cards stretch to row height.RemoveEmailRecoveryconfirmation dialog mirroringRemovePasskey.svelte— "Remove recovery email?" + two short paragraphs + stacked danger / tertiary buttons. Removing an email previously had no warning at all.PR Stack
Test plan
🤖 Generated with Claude Code