Skip to content

feat(onboarding): A4 — import flow (ImportPhrase → PIN → commit)#30

Merged
temrjan merged 7 commits into
mainfrom
feat/a4-import-flow
Jul 8, 2026
Merged

feat(onboarding): A4 — import flow (ImportPhrase → PIN → commit)#30
temrjan merged 7 commits into
mainfrom
feat/a4-import-flow

Conversation

@temrjan

@temrjan temrjan commented Jul 8, 2026

Copy link
Copy Markdown
Member

Что и зачем (A4 · Этап 2, последний A-слайс)

Восстановление кошелька записанной 12-словной BIP-39-фразой: Welcome → ImportPhrase → CreatePin → ConfirmPin → commit → unlocked. Активирует инертную кнопку «Импортировать фразу». Закрывает go-live-блокер #21 (KPI-recovery — после потери устройства/ключа фраза снова ведёт к кошельку).

Второй из двух PR арки; core-часть (validate_mnemonic) уже на main (rustok-org/core#85), сабмодуль бампнут на неё.

Ключевое

  • Валидность решает ТОЛЬКО Rust FFI (validateMnemonic, без KDF, честный тег InvalidInput). JS-словарь — лишь подсказка (полоса автодополнения + подчёркивание), не гейт.
  • Экран ввода = самая чувствительная поверхность: фраза живёт только в JS-памяти, стирается на success/cancel/back; анти-утечка-пропсы на всех 12 полях (visible-password глушит Gboard-подсказки); ноль чтений клипборда/сети/логов/persist.
  • Paste — только OS-нативный: пробел-в-конце фиксирует слово; multi-word раскладывается по полям; >12 слов → ошибка целиком (без обрезки).
  • Полоса автодополнения под 3 замками: локальный словарь / без кэша / только явный tap.
  • Таксономия ошибок коммита: exists / ffi / storage — три различимых сообщения + Retry.

Гейты

  • npm run typecheck → чисто
  • npm run lint → 0 ошибок (изменённые файлы 0/0)
  • npm test196 passed / 196 (+46 новых; покрытие phraseInput+onboardingStore 100%, ImportPhrase 95.8%, ConfirmPin 85.7%)
  • Red→green доказан мутациями (prefix-guard, lowercase, таксономия-collapse, invalid-дискриминация, overflow index-blind, словарь-корозия)
  • Device-smoke 8/8 при Капитане (round-trip chore(deps-dev): bump js-yaml from 3.14.2 to 3.15.0 #21: создать→wipe→импорт→адрес совпал; Gboard-подсказки отсутствуют)

Гейт-1 и Гейт-2 (fleet 5 линз + security ЧИСТО) — оба APPROVED; МИНОРы вшиты. Mobile CI = workflow_dispatch (заглушён, #3/#16) → мерж по локальному зелёному, build-android остаётся #16.

Вне скоупа (записано): #22 (ru→en sweep существующих экранов), #19 (FLAG_SECURE для ImportPhrase), #23 (вход без биометрии).

temrjan added 7 commits July 8, 2026 11:01
Bumps vendor/core bc70f07 -> 13e2330 (core#85: validate_mnemonic) and
regenerates the uniffi bindings (ubrn build android --and-generate, arm64-v8a).
Machine-generated diff only — no hand-written code — so the manual A4 commits
that follow (phraseInput, store, screen) stay reviewable on their own.

validateMnemonic(phrase: string): void /*throws*/ now present in generated TS;
tsc clean; old jest 150/150 (regen broke nothing).
…ber)

Pure input-shaping for the import screen: splitPastedPhrase mirrors the core
normalization (trim/lowercase/whitespace-collapse) for OS paste; suggestWords
powers the autocomplete strip (>=2-char prefix, capped, stateless — no
per-prefix cache per strip lock 2/NIT-5); isWordlistMember drives the per-cell
underline. None gate validity — the Rust FFI is the only judge. 16 tests,
red->green proved by mutating the prefix guard and the lowercase step.
…ract mock

Adds the import path to the step machine: flow='import', beginImport,
submitImportPhrase (Rust FFI is the only validity judge — InvalidInput->'invalid',
unexpected throw->'error'), and completeImport with an honest failure taxonomy
(exists/ffi/storage — NOT the create flow's catch{return 'error'}). confirmPin
now branches by flow: create backs up (ShowPhrase/Quiz), import skips straight to
commit. Jest bridge mock gains validateMnemonic + the tagged FfiError namespace.

24 store tests (11 new) + 24 deviceWallet green; red->green proved by collapsing
the commit taxonomy and the invalid/error discrimination. tsc's navigator
exhaustiveness for 'importPhrase' is closed by T5 (screen + navigator case).
ImportPhrase: 12-field grid with anti-leak TextInput props (visible-password
suppresses Gboard suggestions), OS-paste layout with a whole-reject on >12 words
(no silent truncation), a local-wordlist autocomplete strip (explicit-tap only,
no cache), and a membership underline hint — none of which gate validity (the
Rust FFI does). ConfirmPin becomes flow-aware: create is unchanged; import
commits the already-validated phrase after the PIN, with a spinner and an honest
per-class error + Retry. Navigator routes the new importPhrase step.

9 ImportPhrase tests + 2 navigator import-path tests (happy + commit-fail/retry).
…rdlist

Welcome's 'import' button now enters the import flow (beginImport) instead of
sitting inert. Adds a corruption pin over BIP39_ENGLISH (2048 words, abandon…zoo,
FNV-1a fingerprint) so a silently dropped/reordered word fails CI rather than
quietly degrading the strip/underline hints. red->green proved by mutating one word.
Pins the three distinct commit-failure messages (exists/ffi/storage) by driving
the real completeImport with a per-class commitWallet rejection. Lifts ConfirmPin
branch coverage 76.9%->92.3%.
…ck+overflow tests

M1: test hardware-back → cancel() on ImportPhrase (raw fields never survive).
M2: pin the index+tokens overflow guard with a paste from a non-zero field
    (an index-blind regress would otherwise pass green).
M3: mock validateMnemonic accepts all valid BIP-39 lengths (12/15/18/21/24) —
    stop baking the UI's 12-only rule into the FFI contract (real FFI is agnostic).
M4: SPINNER_COLOR exported from theme.ts, used by ConfirmPin + ImportPhrase.
N5: importErrorMessage gets an assertNever default (repo exhaustiveness idiom).
N6: assert cancel() wipes a validated import mnemonic + resets flow.

jest 196/196; M2 red->green proved by mutating the guard to index-blind.
@temrjan temrjan merged commit a984701 into main Jul 8, 2026
@temrjan temrjan deleted the feat/a4-import-flow branch July 8, 2026 07:00
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.

1 participant