Skip to content

[austin] Checkpoint Igloo UI polish stack#7

Merged
AustinKelsay merged 53 commits into
devfrom
austin/ui-refactor
Jul 5, 2026
Merged

[austin] Checkpoint Igloo UI polish stack#7
AustinKelsay merged 53 commits into
devfrom
austin/ui-refactor

Conversation

@AustinKelsay

@AustinKelsay AustinKelsay commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

  • checkpoint the coordinated Igloo UI refactor stack across Paper references, shared runtime/UI packages, mobile, and consuming clients
  • update parent submodule pointers for the current child PR heads
  • carry the ECDH served-log runtime follow-up through bifrost-rs -> igloo-shared -> browser WASM consumers
  • refresh the browser-WASM source stamp and Chrome vendored bridge artifact so CI guard state matches the latest Rust WASM inputs
  • polish PWA visual/smoke harness helpers for the Paper dashboard selector and runtime-connected states
  • remediate the latest review pass: correlate ping failures by failed peer, clarify threshold-one readiness copy, and merge the PWA dev fixture commit to clear the parent submodule conflict

Child PRs

Validation

  • make igloo-paper-verify STRICT=1
  • npm --prefix test run test:guards
  • npm --prefix repos/igloo-shared run test:unit -- src/wasm-bridge-node.test.ts
  • npm --prefix repos/igloo-shared run test:unit
  • npm --prefix repos/igloo-shared run test:typecheck
  • npm --prefix repos/igloo-ui test -- test/OperatorPanels.test.tsx
  • npm --prefix repos/igloo-ui test
  • npm --prefix repos/igloo-pwa run test:unit:raw
  • npm --prefix repos/igloo-pwa run build:app
  • make verify
  • git merge-tree --write-tree HEAD origin/dev

Manual Runtime Wash

  • verified ECDH served events reach the PWA event log as ECDH share sent
  • verified Respond ECDH Ask opens an approval prompt for the receiving peer
  • verified approve completes the first request and deny fails the next request with operator denial

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 934ab935-37ea-4355-87a7-f8ce468722ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Seven submodule pointers are bumped. The Playwright test harness gains three new live runtime specs (permissions override, recover execution, welcome unlock), reworked visual snapshot builders parameterized by peer permission states, extensively expanded page-object helpers, a new opt-in test:guards:visual:captures aligned-output guard, and updated visual manifest entries. Dev plans, reports, and backlog are updated to reflect landed telemetry and the Settings sponsor onboarding redesign.

Changes

Test Harness Expansion and Visual Coverage

Layer / File(s) Summary
Shared test support
test/shared/browser-artifacts.ts, test/igloo-pwa/support/shell-signer.ts, test/igloo-pwa/support/ui.ts
createGeneratedBrowserArtifacts accepts an optional privateKey; startShellSigner gains optional runtimeOptions that are written into the profile JSON before daemon start; expectPwaRuntimeConnected and expectPwaSignerSignReady updated to match redesigned UI selectors.
Page-object API
test/igloo-pwa/support/pages.ts, test/igloo-pwa/support/flows.ts
fillRotateSource switches from bfshare to sourcePackage; removes selectMode/copyGroupKey; adds expectGenerateForm, expectSelectShareHidden, expectNoDashboard, expectRoute; substantially expands dashboard-state validators, recover/settings helpers, permissions token helpers, replace-share/export/onboard modal interactions, unsaved-guard flows, and readCssBackgroundAlpha utility.
Aligned-capture guard
test/scripts/check-pwa-visual-manifest.mjs, test/scripts/check-pwa-visual-manifest-negative.sh, test/scripts/report-pwa-visual-comparison.mjs, test/package.json, test/README.md, test/docs/WORKFLOWS.md
Adds FROSTR_PWA_VISUAL_REQUIRE_ALIGNED_OUTPUTS mode: checks aligned screen output files exist and rejects uncertainty notes; adds test:guards:visual:captures npm script; extends negative test; adds Notes rendering to comparison report; updates README and workflow docs.
Visual manifest
test/igloo-pwa/visual-manifest.json
Promotes dashboard-signer to aligned; adds dashboard settings/security/onboard/replace-share screens; adds create-validation-error, import-existing-device, rotate-collect-shares; adds DEV-seam notes to import/onboard save-profile entries.
dashboard-visual and settings-visual specs
test/igloo-pwa/specs/dashboard-visual.spec.ts, test/igloo-pwa/specs/settings-visual.spec.ts
Introduces deterministic paper key hex constants, peerState(), and reworked snapshot builders accepting peerPermissionStates; all dashboard visual tests seed peer states; adds viewport-alignment layout test. Settings spec adds buildReplaceShareVisualConnection, init-script injectors, captureViewport, and tests for onboard-handoff modal, narrow sidebar, and replace-share transient states.
permissions-visual and app-shell specs
test/igloo-pwa/specs/permissions-visual.spec.ts, test/igloo-pwa/specs/app-shell.spec.ts
Permissions spec replaces fixed snapshot with peer-parameterized builder and adds CSS token color assertions. App-shell spec adds seedLockedGeneratedDashboard/openUnlockedDashboard helpers plus tests for create-draft restore, focus preservation, deep-link routing, settings/permissions persistence through reload+unlock, and nav-tab alignment.
New live runtime specs
test/igloo-pwa/specs/permissions-runtime-live.spec.ts, test/igloo-pwa/specs/recover-execution.spec.ts, test/igloo-pwa/specs/welcome-unlock-live.spec.ts
Three new @live Playwright specs: permissions override denying respond/sign and asserting subsequent signing failure; full 2-of-3 threshold recovery wizard asserting NSEC output; stored-profile welcome unlock against a cooperating shell peer.
Other visual spec updates
test/igloo-pwa/specs/welcome-visual.spec.ts, test/igloo-pwa/specs/import-visual.spec.ts, test/igloo-pwa/specs/onboard-visual.spec.ts, test/igloo-pwa/specs/recover-visual.spec.ts, test/igloo-pwa/specs/rotation-create.spec.ts, test/igloo-pwa/specs/rotation-update.spec.ts
Welcome spec adds rotate directory, modulo member index fix, create-validation-error and rotate-collect-shares tests. Import/onboard specs add DEV-seam visual state objects and "Import Existing Device" heading. Recover visual uses page-object navigation. Rotation specs switch to p.welcome.rotate/sourcePackage and assert localStorage instead of UI key text.

Dev Docs, Plans, Reports, and Submodules

Layer / File(s) Summary
Submodule bumps
repos/bifrost-rs, repos/igloo-chrome, repos/igloo-home, repos/igloo-paper, repos/igloo-pwa, repos/igloo-shared, repos/igloo-ui
Seven submodule commit pointers updated to new revisions.
Plans, reports, and backlog
dev/BACKLOG.md, dev/plans/bifrost-rs-peer-telemetry-and-approval-spec-2026-06-10.md, dev/plans/dashboard-settings-export-paper-redesign-2026-06-01.md, dev/plans/igloo-ui-remaining-paper-sync-hard-cut-plan-2026-05-25.md, dev/plans/settings-onboard-sponsor-unblock-plan-2026-06-19.md, dev/reports/...
Backlog pruned; telemetry spec updated to mark (a)–(c) complete and shift focus to approval queue; export plan adds password-only constraint; Paper sync plan replaces onboard-sponsor/* references with dashboard modal screens; new settings-onboard-sponsor unblock plan documents four-phase architecture; new settings sidebar audit report added; paper design system investigation updated with artboard count and export status.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 Hoppity-hop through the test lane I go,
Seeding peer states with a deterministic flow,
Live specs for unlock, recover, and deny—
Each snapshot aligned as the captures roll by.
The manifest grows with new screens to compare,
A visual guard checks that no capture is bare! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the coordinated Igloo UI polish stack checkpoint and reflects the main change set.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch austin/ui-refactor

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.

@AustinKelsay
AustinKelsay marked this pull request as ready for review June 28, 2026 15:18

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/igloo-pwa/specs/app-shell.spec.ts (1)

70-103: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the stored-profile fixtures aligned with the shared seed contract.

These manual seeds now disagree with createPwaStoredProfileSeed(...): seededDashboardProfile() encodes group_public_key = '22'.repeat(32) but group_package_json.group_pk = "22", and the inline persisted profile uses relay_profile: 'browser' even though shared seeds derive that field from relays[0] (or 'local'). That means the reload/unlock coverage here is exercising impossible persisted state and can miss profile-hydration regressions. Prefer building these fixtures through the shared helper, or at least make group_package_json.group_pk and relay_profile match the real saved shape.

Also applies to: 236-243

🤖 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 `@test/igloo-pwa/specs/app-shell.spec.ts` around lines 70 - 103, The
stored-profile fixtures in app-shell.spec.ts are diverging from the shared seed
shape, so update the test data to match createPwaStoredProfileSeed and the real
persisted profile contract. In seededDashboardProfile(), make
group_package_json.group_pk consistent with group_public_key, and ensure
relay_profile is derived from relays[0] (or local) instead of hardcoding a
mismatched value; likewise, update the inline persisted profile fixture to use
the same shared helper or equivalent fields. This keeps the reload/unlock tests
aligned with the actual hydration path and the seeded profile symbols used here.
🧹 Nitpick comments (1)
test/igloo-pwa/support/shell-signer.ts (1)

97-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow runtimeOptions to the supported profile fields.

Record<string, unknown> lets misspelled keys silently land in the profile JSON, so the live specs fail later with a timeout instead of a type error. A small typed shape here would catch that at compile time.

Proposed change
+type ShellRuntimeOptions = Partial<{
+  sign_timeout_secs: number;
+  ping_timeout_secs: number;
+  request_ttl_secs: number;
+  state_save_interval_secs: number;
+}>;
+
 export async function startShellSigner(input: {
   bfprofile: string;
   packageSecret: string;
   relayUrl: string;
   label?: string;
-  runtimeOptions?: Record<string, unknown>;
+  runtimeOptions?: ShellRuntimeOptions;
 }): Promise<ShellSigner> {
🤖 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 `@test/igloo-pwa/support/shell-signer.ts` around lines 97 - 103, The
startShellSigner input currently uses a generic Record<string, unknown> for
runtimeOptions, which allows unsupported or misspelled fields to slip into the
generated profile JSON. Replace it with a narrow typed interface or type alias
that lists only the supported profile fields, and update the startShellSigner
signature and any related usage to accept that shape. Keep the change centered
around startShellSigner so compile-time errors catch invalid runtimeOptions keys
before the live specs fail.
🤖 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 `@test/docs/WORKFLOWS.md`:
- Around line 77-80: The WORKFLOWS guidance for test:guards:visual:captures is
incomplete because it only mentions generated .tmp artifacts and misses that the
same guard can fail on aligned-note text. Update the instructions around
test:guards:visual:captures and check-pwa-visual-manifest.mjs to explicitly
mention that the guard also rejects aligned entries whose notes match
ALIGNED_UNCERTAINTY_PATTERNS, so failures from stale uncertainty notes are
covered alongside .tmp artifacts.

In `@test/igloo-pwa/specs/dashboard-visual.spec.ts`:
- Line 338: The timestamp in the dashboard visual fixture is inconsistent with
the surrounding `events` values, which are in seconds. Update the affected event
entry in `dashboard-visual.spec.ts` so it uses a seconds-based timestamp like
the other items in the `events` array, keeping the fixture format consistent for
ordering and rendering.

In `@test/igloo-pwa/specs/rotation-create.spec.ts`:
- Line 81: The final dashboard verification in rotation-create.spec.ts is too
weak because the checks around the dashboard assertions no longer verify the
deterministic profile-label state set earlier in the test. Update the end-state
checks in the rotation/onboarding flow to assert the expected profile-label
values in addition to using p.dashboard.expectDashboard(), and keep those
assertions in the same final validation points so regressions in landing on the
wrong profile are caught.

In `@test/igloo-pwa/specs/settings-visual.spec.ts`:
- Around line 311-338: The replace-share visual spec is registering multiple
page.addInitScript-based state injections in one flow, which can persist across
reloads and make snapshot order flaky. Update the test around
injectReplaceShareVisualState, seedState, and the applying/failed/success
captures so it uses a single init-script bridge for the whole sequence, or split
those states into separate tests/pages to avoid accumulated scripts.

In `@test/igloo-pwa/support/pages.ts`:
- Around line 465-481: The expectPeerPermission helper currently only checks
that a permission token is visible, so it can pass even when the active
selection never changed. Update expectPeerPermission in pages.ts to assert the
selected/active state for the matching token in the Permissions tab, using the
same direction/method/state locator and the relevant stateful attribute or
accessibility state rather than visibility alone. Keep toggleFirstPeerPermission
unchanged and make the expectation verify the actual active chip styling/state.

In `@test/igloo-pwa/support/ui.ts`:
- Around line 189-190: The `expectPwaSignerSignReady` helper is matching the
first exact “Ready” text anywhere on the page, which can allow the check to pass
on an unrelated element. Update this helper to scope the lookup to the dashboard
surface used by the signer flow, using the existing `Page`-based locator chain
in `expectPwaSignerSignReady` so it only asserts the intended card or container
is ready. Keep the timeout behavior the same, but narrow the selector so other
dialogs or cards with the same label do not satisfy the probe early.

---

Outside diff comments:
In `@test/igloo-pwa/specs/app-shell.spec.ts`:
- Around line 70-103: The stored-profile fixtures in app-shell.spec.ts are
diverging from the shared seed shape, so update the test data to match
createPwaStoredProfileSeed and the real persisted profile contract. In
seededDashboardProfile(), make group_package_json.group_pk consistent with
group_public_key, and ensure relay_profile is derived from relays[0] (or local)
instead of hardcoding a mismatched value; likewise, update the inline persisted
profile fixture to use the same shared helper or equivalent fields. This keeps
the reload/unlock tests aligned with the actual hydration path and the seeded
profile symbols used here.

---

Nitpick comments:
In `@test/igloo-pwa/support/shell-signer.ts`:
- Around line 97-103: The startShellSigner input currently uses a generic
Record<string, unknown> for runtimeOptions, which allows unsupported or
misspelled fields to slip into the generated profile JSON. Replace it with a
narrow typed interface or type alias that lists only the supported profile
fields, and update the startShellSigner signature and any related usage to
accept that shape. Keep the change centered around startShellSigner so
compile-time errors catch invalid runtimeOptions keys before the live specs
fail.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29deace8-0404-41e2-92e2-487e61ea5ee3

📥 Commits

Reviewing files that changed from the base of the PR and between fc03e26 and 5699c60.

📒 Files selected for processing (40)
  • dev/BACKLOG.md
  • dev/HISTORY.md
  • dev/plans/bifrost-rs-peer-telemetry-and-approval-spec-2026-06-10.md
  • dev/plans/dashboard-settings-export-paper-redesign-2026-06-01.md
  • dev/plans/igloo-ui-remaining-paper-sync-hard-cut-plan-2026-05-25.md
  • dev/plans/settings-onboard-sponsor-unblock-plan-2026-06-19.md
  • dev/reports/igloo-ui-paper-design-system-investigation-2026-05-20.md
  • dev/reports/settings-sidebar-alignment-audit-2026-06-19.md
  • repos/bifrost-rs
  • repos/igloo-chrome
  • repos/igloo-home
  • repos/igloo-paper
  • repos/igloo-pwa
  • repos/igloo-shared
  • repos/igloo-ui
  • test/README.md
  • test/docs/WORKFLOWS.md
  • test/igloo-pwa/specs/app-shell.spec.ts
  • test/igloo-pwa/specs/dashboard-visual.spec.ts
  • test/igloo-pwa/specs/import-visual.spec.ts
  • test/igloo-pwa/specs/onboard-visual.spec.ts
  • test/igloo-pwa/specs/permissions-runtime-live.spec.ts
  • test/igloo-pwa/specs/permissions-visual.spec.ts
  • test/igloo-pwa/specs/recover-execution.spec.ts
  • test/igloo-pwa/specs/recover-visual.spec.ts
  • test/igloo-pwa/specs/rotation-create.spec.ts
  • test/igloo-pwa/specs/rotation-update.spec.ts
  • test/igloo-pwa/specs/settings-visual.spec.ts
  • test/igloo-pwa/specs/welcome-unlock-live.spec.ts
  • test/igloo-pwa/specs/welcome-visual.spec.ts
  • test/igloo-pwa/support/flows.ts
  • test/igloo-pwa/support/pages.ts
  • test/igloo-pwa/support/shell-signer.ts
  • test/igloo-pwa/support/ui.ts
  • test/igloo-pwa/visual-manifest.json
  • test/package.json
  • test/scripts/check-pwa-visual-manifest-negative.sh
  • test/scripts/check-pwa-visual-manifest.mjs
  • test/scripts/report-pwa-visual-comparison.mjs
  • test/shared/browser-artifacts.ts

Comment thread test/docs/WORKFLOWS.md Outdated
Comment on lines +77 to +80
After running `npm --prefix test run test:e2e:igloo-pwa:visual`, run
`npm --prefix test run test:guards:visual:captures` before generating or sharing
the comparison report. This opt-in guard checks only generated `.tmp` artifacts,
so it is not part of the always-on manifest-shape guard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify that this command can also fail on aligned-note text.

test:guards:visual:captures still runs check-pwa-visual-manifest.mjs, and that checker now also rejects aligned entries whose notes match ALIGNED_UNCERTAINTY_PATTERNS at test/scripts/check-pwa-visual-manifest.mjs Lines 107-112. Saying it checks only generated .tmp artifacts will misdirect anyone debugging a failure from stale uncertainty notes.

🤖 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 `@test/docs/WORKFLOWS.md` around lines 77 - 80, The WORKFLOWS guidance for
test:guards:visual:captures is incomplete because it only mentions generated
.tmp artifacts and misses that the same guard can fail on aligned-note text.
Update the instructions around test:guards:visual:captures and
check-pwa-visual-manifest.mjs to explicitly mention that the guard also rejects
aligned entries whose notes match ALIGNED_UNCERTAINTY_PATTERNS, so failures from
stale uncertainty notes are covered alongside .tmp artifacts.

Comment thread test/igloo-pwa/specs/dashboard-visual.spec.ts Outdated
Comment thread test/igloo-pwa/specs/rotation-create.spec.ts Outdated
Comment thread test/igloo-pwa/specs/settings-visual.spec.ts Outdated
Comment thread test/igloo-pwa/support/pages.ts Outdated
Comment thread test/igloo-pwa/support/ui.ts Outdated
AustinKelsay and others added 26 commits June 28, 2026 22:20
- Add device_name to OnboardProfileMaterial with serde(default)
- Thread device_name through material_from_onboard_payload,
  material_result_from_profile, recover_profile
- FfiApp::export_profile carries resolved carrier label
- 7 Rust integration tests in export_recipe.rs:
  - profile round-trip preserves group members + device_name
  - share round-trip preserves secret bytes
  - both reject wrong password (VAL-SET-015)
  - empty-name legacy fallback sentinel
  - missing active material returns error:no_active_profile
- Cargo examples: export_decode, export_smoke
- Validation scripts: verify-export-artifact.sh, iOS/Android focus shells
- Maestro flow: export-focused-paste-back.yaml
- Export recipe docs for validators
- Regenerate Android UniFFI bindings for deviceName property
- Clippy fixes in updates.rs and nostr_connect_smoke.rs

Refs: VAL-SET-015, VAL-LOAD-001..019, VAL-CROSS-004/007/009, VAL-SHELL-012/016
Fixes critical regression from 8e02b5a where signer startup was broken
on both iOS and Android because dashboard.profile_info was never populated.

- OnboardStored: populate profile_info from onboarding.resolved before navigating to Dashboard
- LoadProfileStored: populate profile_info from load_profile.resolved before navigating to Dashboard
- OpenProfile: seed minimal profile_info and emit RestoreFromSecureStorage side effect
- iOS AppManager: handle RestoreFromSecureStorage by loading full material from Keychain
- Android AppManager: handle RestoreFromSecureStorage by loading full material from EncryptedSharedPreferences

Tests: 177 passed, clippy clean.
… rows

The mobile-create-keyset-flow feature wires the bridge-supplied
`events_len` through to the actor. The previous code discarded it in
`AppAction::SignerStatusUpdate` and hardcoded `cache.events_len = 0`
on every poll, so the dashboard event log could never reflect a real
runtime transition even when the bridge reported a fresh event.

The actor now tracks the highest `events_len` it has ingested in a
new `SignerRuntimeState.runtime_observed_events_len` field. When the
shell-supplied count advances past that baseline exactly one safe INFO
row is prepended with an RFC-3339 timestamp; unchanged or recovered
counts never duplicate the row, and existing Rust-emitted entries
stay intact in newest-first order.

The polling task mirrors the change. It computes a fingerprint of the
observable runtime metadata (readiness, relay-connected, peer-online
map, pending-ops count) and advances `cache.events_len` by one each
time the fingerprint differs from the previous poll. Because
bifrost-bridge-tokio does not expose a per-event payload stream, the
fingerprint is the local substitute that keeps the runtime event log
truthful without fabricating payload-bearing rows. The fingerprint
resets on `stop_signer` so a fresh `start_signer` always registers
as one event.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…up workers

The mobile-create-keyset-flow feature wires bridge-supplied events_len
through the actor, dedupes per-tick INFO rows, and mirrors the change
in the polling task's runtime fingerprint tracker. The companion note
documents both the actor contract (runtime_observed_events_len field
and the comparison logic in AppAction::SignerStatusUpdate) and the
polling-task contract (cache.events_len, fingerprint builder, and
last_obs_fingerprint reset on stop_signer), plus the
binding-regeneration requirement so later Android/iOS work does not
miss the regenerated data class FfiConverter read/write helpers.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…t, iOS, Android

Adds the full Create / Rotate Keyset create-mode wizard covering
Generate parameters and validation, Device Profile without local-storage
password (share picker + relays), Review accuracy, Accept storing/starting
the local profile, Distribute per-share copy/QR/save forms with
password-gated bfonboard1 outputs and status chips, Finish routing
to dashboard, abandonment semantics, and busy feedback during heavy
Argon2id work. Fulfills VAL-CREATE-001 through VAL-CREATE-022.

Includes:
- Rust: state/keyset.rs with KeysetFlowState, KeysetFlowStep,
  KeysetValidationError, GeneratedShare, KeysetBundleRecord,
  DistributeStatus, DistributeShareRecord; 17 new AppAction variants;
  4 new AppUpdate side effects; FfiApp::generate_keyset and
  FfiApp::encode_distribute_onboard; 4 new state-machine tests.
- iOS SwiftUI: CreateKeysetEntry/Generate/DeviceProfile/Review/
  Distribute views with StepProgressStrip, ModeChip,
  DistributeStatusChip, QrCodeModal + QrCodeImage (CoreImage).
- Android Compose: full Create Keyset screens with mode selector,
  share picker, distribute forms, status chips, zxing QR dialog;
  AppManager.kt shell helpers for the four new AppUpdate variants.

Verification: cargo test (145 cases passed), cargo fmt --check,
cargo clippy -D warnings, rmp bindings --check all, iOS xcodebuild
iphonesimulator, Android assembleDebug.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…id, and library

Implements the QR affordances that bind the Create Keyset Distribute step
(modal QR display via existing CoreImage / zxing renderers re-validated
here) and the OnboardDevice connect flow's new Scan QR entry point.

- VAL-QR-001 — Create Keyset Distribute step's QrCodeModal (iOS) and
  QrDialog (Android) render the bfonboard1 envelope as a real scannable QR.
  The modal payload text surfaces via accessibilityIdentifier
  qr_payload_text (iOS) + testTag qr_payload_text (Android) so the legacy
  hierarchy dump can recover the full envelope.

- VAL-QR-002 — Scan QR sheet degrades on iOS Simulator + Android emulator
  with no working rear camera. iOS probes AVCaptureDevice.default(for:
  .video) (nil on Simulator); Android gates on Build.FINGERPRINT
  (generic / sdk / emulator / google_sdk) plus CameraManager.cameraIdList
  + LENS_FACING_BACK to keep AVD rear-camera false-positives out of the
  real-camera branch.

- VAL-QR-003 — Paste fallback feeds the trimmed bfonboard text into the
  existing OnboardConnect state via the onScanned / onUsePayload closures.
  The actor's trim + OnboardConnect + performOnboardHandshake chain
  treats the resulting state identically to manual paste.

- Library user-testing.md gains a QR Decode Recipe section documenting
  the canonical zbarimg invocation + optional ImageMagick crop rotation
  for screenshot decode.

- Two focused Rust regression tests in state_machine_tests.rs pin the
  canonical bfonboard1 envelope shape and the trim-equivalence contract
  across QR paste + manual paste. Both tests use placeholder
  hex-shaped fields (PLACEHOLDER_CREDENTIAL constant) following the same
  fixture pattern as bifrost-rs' own BfOnboardPayload tests.

- Two focused Maestro flows (qr-scan-android.yaml / qr-scan-ios.yaml)
  drive Hub -> OnboardEntry -> OnboardConnect -> Scan QR -> fallback ->
  paste -> Use This Package -> password -> Connect -> review -> Save ->
  dashboard against the live demo stack.

Build / lint / test results:
- cargo test --workspace: 147 + 8 + 7 + 24 cases pass (186 total, includes
  the 2 new QR regression tests).
- cargo fmt --check: clean.
- cargo clippy --workspace --all-targets -- -D warnings: clean.
- iOS xcodebuild build (Debug, iphonesimulator, arm64): BUILD SUCCEEDED.
- Android assembleDebug: BUILD SUCCESSFUL.
- Maestro qr-scan-ios.yaml: all steps COMPLETED on iOS RMP iPhone 15 / 26.5.
- Maestro qr-scan-android.yaml: all steps COMPLETED on rmp_api35.

Evidence in apps/igloo-mobile/library/evidence/VAL-QR-mobile-qr-display-and-scan-support/.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…droid

Brings RMP rotate-share behavior online end-to-end on the shared Rust
TEA core plus the iOS SwiftUI and Android Compose shells.

Rust core ():
- New  typed state (,
  , , ) and
   rotation-source picker (,
  ).
- New AppAction variants (Open/Update/Connect/HandshakeSuccess/
  HandshakeFailure/Replace/ClearError/Reset + Keyset*RotationSource*)
  and AppUpdate side-effect variants
  (PerformRotateShareHandshake, ReplaceProfileFromRotate,
  PerformKeysetRotation).
- Typed-error mapping, group/same-profile preemptive rejection,
  rotate-mode Generate validation gate,
  rewrite helper, and 28 red-step tests in
  .

FFI: , ,
, ,
plus /sfot decoder. Swift and Kotlin
UniFFI bindings regenerated ( gitignored;
 tracked).

iOS shell ():
- AppManager convenience methods, AppUpdate apply-handlers for
  performRotateShareHandshake (off-main Thread reusing
  rust.onboard) and replaceProfileFromRotate (Keychain +
  profile-index swap).
- New  +  with stable
  accessibility identifiers for the iOS simulator Maestro flows.

Android shell ():
- AppManager methods + reconcile cases for
  PerformRotateShareHandshake (off-main) and ReplaceProfileFromRotate
  (EncryptedSharedPreferences swap + hub status update).
-  +  Compose
  surfaces with matched testTags gating the
   route.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ation + round-trip recovery

Materialization paths in igloo-mobile-core (OnboardStored, LoadProfileStored,
CreateKeysetAccepted, RotateShareReplace) now publish a NIP-44 encrypted
kind-10000 backup event to the profile's configured relay using
frostr-utils's create_encrypted_profile_backup + build_profile_backup_event
(reused unchanged from the WASM bridge; reimplementation is out of policy).

The combined side-effect variant for create + rotate keeps the actor's
single side-effect-per-update pattern intact while still emitting the
backup publish alongside the storage swap. The dashboard mirrors the
publish result via BackupPublishStatus so a validator trace sees the
share-derived author and relay count without instrumenting the actor.

FfiApp::recover_profile is rewired to fetch the latest matching kind-10000
event from the relay via a Nostr REQ filtered by the share-derived
x-only author, decrypt it with frostr_utils::parse_profile_backup_event,
and rebuild a full multi-member OnboardProfileMaterial so the round-trip
closes cleanly against a freshly-published backup.

Tests:
- cargo test --workspace: 147 passed; 0 failed
- cargo test -- --ignored live tests: 3 passed; 0 failed (publish +
  relay-side filtered proof + round-trip recovery)
- cargo fmt --check + cargo clippy -D warnings: clean
- uniffi-bindgen kotlin + swift: generates with new types

Includes scripts/verify-relay-backup.py for validator replay of the
relay-side filtered proof and library/evidence/... with captured
unit + live test logs.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ine suite

Adds the production-grade Rust test suite for VAL-CROSS-005 and
VAL-CROSS-008 — the cross-platform keyset interop and rotation
lifecycle assertions. Replaces an earlier broken fixture (compile
errors + logic failures) with a clean suite:

VAL-CROSS-005 (cross-platform keyset interop via copied bfonboard):
  * bfonboard1 envelope round-trips losslessly across the same
    Rust path running on iOS and Android shells.
  * iOS-minted envelopes decode on the Android path with identical
    group pubkey; Android-minted envelopes decode on iOS.
  * Wrong-password decode fails on every platform path.
  * Each direction's decoded share verifies against the source
    bundle's FROSTR group via frostr_utils::verify_share.

VAL-CROSS-008 (cross-device rotation lifecycle):
  * Rotation preserves the group public key byte-for-byte while
    replacing every share public key (zero intersection pre/post).
  * Pre-rotation shares are rejected against the rotated group;
    rotated shares verify against the rotated group.
  * Post-rotation bfonboard1 envelopes decode on the other
    platform's path and verify against the rotated group but not
    the pre-rotation group.
  * Full 64-hex share pubkeys and group pubkeys are emitted for
    direct byte comparison so the contract's pre/post identity
    checks retain fidelity when visual rendering truncates.

Test results on this branch:
  * cross_platform_keyset_interop — 9/9 passed
  * rotate_share_flow — 28/28 passed (pre-existing)
  * cargo fmt --check — clean
  * cargo clippy --workspace --all-targets -- -D warnings — clean

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…s Rust, iOS, Android

Preserve rotated share secret (Rust core):
- Add share_seckey_hex to AppAction::RotateShareHandshakeSuccess and
  RotatePreviewIdentity so the live handshake can carry the rotated
  share secret from the FFI through to the actor.
- build_rotated_material_bytes now requires a 64-hex share_seckey_hex
  on the preview and returns Err on missing or truncated secrets so
  the actor surfaces a typed Unexpected error rather than emitting a
  tombstone-side-effect.
- Test rotate_share_replace_emits_swap_side_effect_and_routes_dashboard
  gains an assertion that the emitted material JSON contains the
  rotated share secret (issue 1); new
  rotate_share_replace_without_share_secret_emits_no_side_effect
  proves the missing-secret path refuses to dispatch.

Regenerate UniFFI bindings (tracked on Android):
- Re-run cargo run -p uniffi-bindgen to capture the new
  ReplaceProfileFromRotateAndPublishBackup side effect, the
  PublishProfileBackup side effect, the BackupPublishResult record,
  and the publishBackup FFI surface. Android binding lands verbatim
  (issue 2); iOS bindings are gitignored but regenerated locally via
  just ios-gen-swift during the build cycle.

iOS AppManager fixes (issues 3 + 4):
- rotateShareConnect dispatches the action only; the duplicate
  off-Main performRotateShareHandshake call is removed so the actor's
  AppUpdate::PerformRotateShareHandshake side effect wins and the
  handshakes stop racing.
- rotateShareReplace dispatches the action only; the empty Data()
  shell-side performReplaceProfileFromRotate call is removed so the
  actor's full material blob (now carrying the rotated share secret)
  reaches secure storage instead of an empty tombstone.
- Reconciler learns the new
  .replaceProfileFromRotateAndPublishBackup case (calls
  performReplaceProfileFromRotate + publishRotatedBackupIfPossible).
- Decoders extract share_seckey_hex from the FFI material blob on
  PerformRotateShareHandshake completion via JSONSerialization.

Android AppManager + Compose fixes (issues 5 + 6):
- New openRotateShareConnect(profileId, shortId, deviceLabel) helper
  dispatches AppAction.OpenRotateShareConnect the same way the iOS
  shell already does.
- Settings tab Rotate Share button dispatches the helper with the
  active profile's id, short id, and device label; falls back to
  navigateToRotateShare when no active profile is open.
- Reconciler learns AppUpdate.ReplaceProfileFromRotateAndPublishBackup
  and AppUpdate.PublishProfileBackup; new performPublishBackup helper
  forwards BackupPublishResult via AppAction.BackupPublishCompleted.
- Initial DashboardState literal sets lastBackupPublish = null so the
  regenerated Kotlin binding compiles end-to-end.

Tests + verification:
- cargo test --workspace: 147 passed (29 rotate_share_flow tests,
  +1 new for missing-secret rejection).
- cargo fmt --check + cargo clippy --workspace --all-targets -D warnings:
  clean.
- just ios-full: iOS simulator build succeeds for arm64 debug.
- just android-full: Android debug build succeeds across all three
  ABIs (arm64-v8a, armeabi-v7a, x86_64).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…le via keyset.accepted_profile_id

CreateKeysetDistributeFinish previously read hub.profiles.first() to find
the profile the user just built. Once multiple profiles are stored on a
device, that list-order assumption breaks: a previously-restored profile,
a rotate-share accept, or any future action that mutates hub order can
leave first() pointing at the wrong row, silently marking the wrong
profile Active.

Add KeysetFlowState.accepted_profile_id and pin it from CreateKeysetAccept
(actor-derived) and CreateKeysetAccepted (shell-reported) so the two
sides cannot diverge. CreateKeysetDistributeFinish now targets that id
explicitly via the same iterate-and-match pattern CreateKeysetAccepted
already uses, with a defensive fallback that re-adds the row if the hub
list was rebuilt between Accept and Finish. Reset clears the pin so a
re-entry starts fresh (VAL-CREATE-021).

State-machine tests pin the contract:

- keyset_finish_targets_accepted_profile_id_not_hub_first (reproduction:
  hub has [alice, new_bob], Finish must mark new_bob Active)
- keyset_finish_routes_to_dashboard_when_accepted_profile_id_is_set
- keyset_finish_falls_back_to_hub_when_accepted_profile_id_unset
- keyset_accept_pins_accepted_profile_id
- keyset_accepted_keeps_accepted_profile_id_in_sync_with_shell

Verified: cargo test --workspace -- --test-threads=9 (152 passed, 0
failed), cargo fmt --check, cargo clippy --workspace -- -D warnings,
rmp bindings --check all, xcodebuild iOS simulator Debug, just
android-full. New UniFFI field requires regenerating Android Kotlin
binding (regenerated and committed); iOS Swift binding is gitignored.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…4 in Android shell

The Android shell's RotateShareConnectScreen pre-filled with
ws://127.0.0.1:8194, which is the iOS Simulator host-loopback value.
On the Android emulator that URL routes into the emulator's own
loopback namespace and never reaches dev-relay, so every Android
user who did not override the form sat on a connection-timeout
that masqueraded as a network problem.

Centralise the Android platform-correct default (10.0.2.2:8194)
through a new RelayDefaults object and route all three call sites
that previously held the literal on disk:

  * MainApp.kt OnboardConnectScreen (was already the right value,
    refactored for single-source consistency)
  * MainApp.kt RotateShareConnectScreen (the actual bug)
  * AppManager.kt injectOnboardCredentials fallback
  * MainActivity.kt companion-object DEFAULT_RELAY_URL,
    now sourced from RelayDefaults.DEFAULT so the Compose forms
    and the debug intent path share one constant

The Rust core is intentionally untouched. Its default_relay_url
helper stays ws://127.0.0.1:8194 (a neutral best-effort for
host-side Rust runs) because the platform-correct default lives in
the shell layer; shells override at form initialization, not on
submit, and both platforms' forms remain fully editable.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…sourceId on keyset OutlinedTextField nodes

Compose semantics modifiers on the wrapped OutlinedTextField controls in
the Create Keyset wizard (Generate, Device Profile), the per-row Distribute
share card, and the Rotate Share connect screen now set
testTagsAsResourceId = true alongside the existing testTag value, so the
underlying android.widget.EditText nodes expose the canonical ids as
resource-id attributes to uiautomator and Maestro. Without this flag
Maestro's id selection cannot resolve the field, producing the Compose
TextField text-entry limitation that was blocking keyset validation
flows.

Canonical ids applied to wrapped OutlinedTextField modifiers (all include
testTagsAsResourceId = true in the same semantics block):

  CreateKeysetGenerateScreen
    - input_group_name
    - input_threshold
    - input_count
  CreateKeysetDeviceProfileScreen
    - input_device_name
    - input_relays
  CreateKeysetDistributeScreen (DistributeShareCard, per row)
    - input_label_<shareIdx>
    - input_password_<shareIdx>
    - input_confirm_password_<shareIdx>
  RotateShareConnectScreen
    - input_package (renamed from input_rotate_package)
    - input_password (renamed from input_rotate_password)
    - input_relays (renamed from input_rotate_relay)

The three affected composables also gain
@OptIn(ExperimentalComposeUiApi::class) so the testTagsAsResourceId
property compiles under the current Compose UI 1.6.x experimental API
gating. testTag is invisible to users, no observable product behavior
changes.

Verified:
- ./gradlew :app:assembleDebug builds without errors.
- uiautomator dumps on emulator-5554 captured under
  apps/igloo-mobile/library/evidence/mobile-android-textfield-testtags-parity-fix/
  show resource-id="input_group_name"|"input_threshold"|"input_count"
  on the Generate screen EditText nodes,
  "input_device_name"|"input_relays" on the Device Profile screen,
  and "input_label_<idx>"|"input_password_<idx>"|"input_confirm_password_<idx>"
  on the Distribute share cards.
- cargo fmt --check, cargo clippy -D warnings, and cargo test
  --workspace (152 passed, 0 failed) all clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…URL scheme

Add a debug-gated iOS URL scheme that drives the full Create Keyset
wizard to Dashboard completion via xcrun simctl openurl, bypassing
Maestro 2.6.0's brittle SwiftUI TextField/Button automation on iOS 26.5.

The URL:
  igloo://test-create-keyset?group_name=X&threshold=N&count=N&device_name=Y&relay=ws://…

is parsed in App.swift::onOpenURL → AppManager.testCreateKeyset →
AppAction::DiagnosticsCreateKeysetRun. The Rust actor runs
frostr_utils::create_keyset inline, builds the OnboardProfileMaterial,
populates dashboard.profile_info, inserts an Active hub row pinned by
keyset.accepted_profile_id, and emits StoreKeysetCreatedProfile. The
shell writes to Keychain via the existing storeKeysetCreatedProfile
handler, then auto-dispatches CreateKeysetDistributeFinish so the URL
scheme lands on Dashboard without any SwiftUI button-tap.

Gating: DEBUG build + IGLOO_KEYSET_DIAGNOSTICS=1 env var. Release
builds never expose the URL scheme and a malformed URL is a no-op.
Mirrors the existing igloo://test-inject (onboarding) and
igloo://test-save-to-dashboard URL schemes.

Validation:
- 13 new Rust state-machine tests for DiagnosticsCreateKeysetRun
  covering happy-path side effect, dashboard identity population,
  accepted_profile_id pin, distribute rows, dashboard route, and
  invalid-input no-ops (threshold>count, threshold<2, blank
  group/device/relay, fully-blank inputs).
- iOS Maestro flow + validation script + evidence directory under
  apps/igloo-mobile/library/evidence/mobile-ios-keyset-debug-url-scheme-2026-06-15/.
- Headline proof: post-injection dashboard screenshot with the
  freshly-built ScriptDevice profile, identity block rendering Share
  /Group Pubkey, and keyset-diagnostics OSLog event captured.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
….DEFAULT on Android

The Android Compose `CreateKeysetDeviceProfileScreen` (Step 2 of 4 in
the Create Keyset wizard) pre-filled its `input_relays` OutlinedTextField
with `ws://127.0.0.1:8194` — the iOS Simulator host-loopback value. The
Android emulator cannot reach the host via `127.0.0.1` (it routes into
the emulator's own loopback namespace), so every Android user who did not
override the form sat on a relay-connection timeout that masqueraded as
a network problem.

`mobile-android-relay-url-platform-default-fix` (commit `bed0c4c`) added
the single-source `RelayDefaults.DEFAULT = "ws://10.0.2.2:8194"` and
routed `OnboardConnectScreen`, `RotateShareConnectScreen`, and
`MainActivity` through it. This follow-up closes the same path on the
Create Keyset wizard's Device Profile step. The shared Rust actor
(`AppAction::CreateKeysetGenerationSuccess`) still pre-fills
`KeysetFlowState.relays = [default_relay_url()]` so the actor stays
platform-neutral; the Android Compose override reads that vector and
rewrites the pure iOS-Simulator prefill to `RelayDefaults.DEFAULT` while
preserving any user-typed relay URL verbatim.

iOS continues to receive `ws://127.0.0.1:8194` from its own shell
(`ContentView.swift` `CreateKeysetDeviceProfileView.onAppear`), which
is the platform-correct value for the iOS Simulator's shared host
loopback namespace. The override lives in Compose, not in the Rust
actor, so the previous fix's rationale about platform-neutral defaults
remains intact.

Validation:
- 4 new `RelayDefaultsTest` cases pin the override contract: pure iOS
  prefill → Android default, empty actor relays → Android default,
  user-typed list preserved verbatim, mixed (user + iOS) list preserved.
  Pre-existing 3 cases still pin `DEFAULT = ws://10.0.2.2:8194` and the
  ws://+port-8194 shape.
- `cargo fmt --check`, `cargo clippy --workspace -- -D warnings`,
  `cargo test --workspace -- --test-threads=9` → 152 passed / 0 failed
  / 0 ignored. No Rust regression.
- `./gradlew :app:testDebugUnitTest --tests com.frostr.igloo.RelayDefaultsTest`
  → 7 passed / 0 failed / 0 ignored.
- `./gradlew :app:assembleDebug` → BUILD SUCCESSFUL (only the pre-existing
  Kotlin/AGP deprecation warnings from unmodified call sites).

Evidence captured at
`apps/igloo-mobile/library/evidence/mobile-android-create-keyset-wizard-relay-url-fix-2026-06-16/`
because no live Android emulator was available in this worker session;
the unit-test + source-level proof above pins the same contract that a
follow-up uiautomator dump will surface (`input_relays` text will read
`ws://10.0.2.2:8194` instead of `ws://127.0.0.1:8194` per the pre-fix
`mobile-android-textfield-testtags-parity-fix/create-keyset-device-profile-scrolled.xml`
dump).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…con parity

* polish(mobile-android-settings-icon-text-parity): replace the text-only
  Maintenance/Save/Logout buttons on the Android Settings tab with a
  MaintenanceRow icon+text composable that mirrors the iOS HStack layout;
  iconography uses the existing [K]/[L]/[+]/[I]/[R] glyph tokens to stay
  inside the design vocabulary without taking on a new Material-icons
  dependency. No functional change to VAL-SET/VAL-ROTATE surface.

* polish(mobile-dashboard-header-accessibility-identifiers): pin
  `dashboard_header_title` and `dashboard_header_subtitle` testTags on
  both shells so the post-restart / VAL-CROSS-002 validator can confirm
  post-onboard identity through the full hierarchy without scrolling
  (orchestrator note after onboarding-and-runtime user-testing round 1).
  Back buttons on the dashboard header get the same `btn_back_dashboard`
  identifier on Android as the iOS SwiftUI view already exposes.

* feat(mobile-cross-parity-16-view-reachability): add the canonical
  Maestro flow that visits every view in the 16-view parity inventory
  through real navigation from the hub (no deep link, no debug URL
  scheme, no app intent). Paired with the existing `hub-validation.yaml`
  shell-baseline flow and the new Android-only system-back companion
  flow.

* feat(mobile-cross-parity-android-system-back): dedicated VAL-SHELL-014
  companion flow that exercises `pressKey: back` end-to-end. The shared
  `cross-parity-16-view-reachability.yaml` never invokes `pressKey: back`,
  so iOS validators no longer risk a system-back step in a shared flow.

* docs(mobile-parity-library): cross-parity-16-view-reachability matrix
  and final-polish-cleanup-notes commit the documented acceptance for the
  orchestrator notes that landed after settings-and-maintenance, the
  iOS layout fix, onboarding-and-runtime round 1, and 971e4d2. Existing
  untracked Maestro flows and services/igloo-demo/entrypoint.sh remain
  user-owned.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ofile override to actor

The Android Create Keyset wizard Device Profile Compose override
(commit ee7234e, mobile-android-create-keyset-wizard-relay-url-fix)
already rewrote the iOS-Simulator localhost prefill (`ws://127.0.0.1:8194`)
to `RelayDefaults.DEFAULT` (Android emulator's `ws://10.0.2.2:8194`)
for *display*. It did not sync that override back to the actor's
`keyset.relays`, so the Review step kept rendering the stale
iOS-Simulator localhost (`ws://127.0.0.1:8194`) even when Device
Profile clearly showed `ws://10.0.2.2:8194`. The mismatch violated
VAL-CREATE-008 ("each displayed value exactly matches what was
entered or generated") and silently poisoned every Android wizard
run's relay list.

Add a one-shot `LaunchedEffect(Unit)` on first composition of
`CreateKeysetDeviceProfileScreen` that dispatches
`AppAction.CreateKeysetUpdateRelays([RelayDefaults.DEFAULT])`
whenever the Compose-side override fires, so the actor carries the
platform-correct value into the Review step and beyond. iOS Compose
needs no change: the actor's iOS-Simulator localhost prefill IS its
platform-correct value, so the override trigger condition never
fires in practice and the sync dispatch stays a no-op there.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ofile_id

Validator report: tapping one hub profile row sometimes opened a
different stored profile's dashboard — the row's openProfile
lambda was being captured from the wrong iteration when the hub
list grew or re-ordered, so the dispatched AppAction.OpenProfile
profile_id did not match the row the user actually tapped.

Root cause: the Android hub iteration was a bare
  for (profile in manager.state.hub.profiles) { ProfileRowView(...) }
loop without a stable key(...). Compose's slot table keys those
positional iterations, so when the list grew (insert-at-front from
NEW onboard, OnboardStored, ProfileRestored, CreateKeysetAccept)
the captured `profile` in each row's onClick lambda could lag behind
the visible row, and a tap landed on a position whose onClick
resolved to a stale profile_id. The iOS shell already uses
`ForEach(..., id: \\.profileId)` for the same reason.

Fix: wrap each ProfileRowView in a `key(profile.profileId) { ... }`
block so the slot-table key is the actual profile_id and the
captured lambda always carries the profile the user can see in that
row. Imports `androidx.compose.runtime.key` once near the existing
runtime imports.

Regression coverage: add `open_profile_routes_to_dashboard_with_matching_identity`
to `apps/igloo-mobile/rust/tests/state_machine_tests.rs` that
exercises the actor's OpenProfile -> dashboard.profile_info
round-trip across A -> B -> A taps, asserting identity parity
between the dispatched profile_id and the dashboard
profile_info.{profile_id, device_name}. Combined with the
shell-side slot-table key, this pins the contract from both ends.

Verification: `just android-full` builds clean;
`cargo test --workspace -- --test-threads=9` reports 153 passed
including the new regression test; `cargo fmt --check` and
`cargo clippy --workspace --all-targets -- -D warnings` clean.
Live hub-row -> dashboard identity verified against the demo-stack
on emulator-5554 (bob as test-r2 / carol as test-cross-003); hierarchy
dumps under `apps/igloo-mobile/library/evidence/mobile-android-hub-profile-row-routing-fix-2026-06-16/`
show `dashboard_header_subtitle` correctly swaps between the two
profile_ids (f1e748b7 <-> 4a36d42d) with no cross-contamination. A
focused Maestro flow under `apps/igloo-mobile/flows/hub-profile-row-routing-fix-android.yaml`
and a paired helper script `apps/igloo-mobile/scripts/inject-android-bob-and-carol-two-profiles.sh`
reproduce the precondition plus execution path so future validators
can re-run the row-tap parity evidence without recreating the harness
out of band.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Add comprehensive README.md with architecture, build, test, and limitation docs
- Add 30+ Maestro validation flows for iOS and Android
- Fix igloo-demo entrypoint to mkdir runtime dir before use
- Replace hardcoded demo passwords with Maestro env var references

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Add helper scripts for Maestro, clipboard, URL injection, verification
- Add onboard_provisioner_offline.rs integration test
- Add services.yaml for mission runtime
- Add tools/ and uniffi-bindgen/ directories
- Add library/evidence/ to .gitignore (generated test artifacts)
- Fix synthetic password in test to avoid false-positive secret detection

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… Sign Ready path

The round-5 mobile-signer-runtime-console validator observed that the
iOS Start signer tap never advanced SignerStatusCard past Stopped: the
Maestro tap resolved to the outer VStack's identifier instead of the
inner Button, leaving the SwiftUI Button action un-fired even when
the click hit-tested successfully.

Two compounding symptoms were identified and fixed:

1. SignerStatusCard identifier shadowing. The outer VStack carried
   .accessibilityIdentifier('signer_status_card'), and iOS 26.5 /
   Maestro 2.6.0 forwarded that identifier into the inner Button,
   shadowing the Button's own .accessibilityIdentifier('btn_start_signer')
   / .accessibilityIdentifier('btn_stop_signer'). The fix moves the
   'signer_status_card' identifier to the visible status Text, keeps
   the Button's own identifier (placed before .buttonStyle(.plain) to
   match the working pattern in ProfileRow), and adds
   .contentShape(RoundedRectangle(...)) so the whole pill stays
   hit-testable.

2. Keychain -> simulator file-fallback asymmetry. On iOS Simulator
   (DEBUG-only), storeProfile falls back to a file under Application
   Support / IglooSimulatorKeychainFallback when Keychain returns
   errSecMissingEntitlement (-34018), but loadProfileMaterial only
   queried the Keychain. The fixed path also consults the same
   fallback file under #if DEBUG && targetEnvironment(simulator),
   and deleteProfileMaterial cleans up the fallback file so a
   re-onboard after a failed upload does not surface stale material.

Verified end-to-end with scripts/run-focus-ios-signer-start-fix.sh:
the app startSigner invokes FfiApp.start_signer with material_len=713,
rust.startSigner returns true, and Sign Ready becomes visible within
the 60 s envelope (simctl log show + maestro-start.log captured in
apps/igloo-mobile/library/evidence/mobile-signer-runtime-validation-followup-2026-06-16/ios-fix/).

The Android parity leg remains open; scripts/run-focus-android-signer-sign-readiness-recovery.sh
captures the remaining failure (Start tap propagates, relay connects,
alice's PONG yields incoming nonces, but Restoring never advances to
Sign Ready over a 90 s envelope + Stop/Start rearm). The autoping
bootstrap and shell merge logic are unchanged from the prior
mobile-signer-runtime-restoring-readiness-fix; the Android-side
follow-up is documented in library/evidence/mobile-signer-runtime-validation-followup-2026-06-16/README.md
so a later worker can diff the iOS success trace against the Android
snapshot matrix.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… NostrSdkAdapter in settle-time shim

The Android signing-path NostrSdkAdapter was the only place in the rust
core that talked to the relay without the explicit settle-window shim
that the (previously onboard-only) VerifiedNostrSdkAdapter wrapper
already supplied. Without those settle windows the autoping PING fired
right after connect()/subscribe() returned, racing the relay pool's
background WS handshake on Android and leaving peer_last_seen None
even after alice's PONG-with-nonces landed.

* Wrap start_signer()'s adapter in
  igloo_mobile_core::signer::VerifiedNostrSdkAdapter::for_signing
  with explicit 1 s / 750 ms / 750 ms settle windows after connect /
  subscribe / publish.
* Tag every tracing event with path="signing" so the signing-path
  timeline is grep-distinguishable from the onboard path.
* Add a once-guarded tracing subscriber (EnvFilter honouring RUST_LOG)
  attached to a custom AndroidLogWriter that pipes tracing events
  through android_logger into adb logcat under tag
  "igloo-mobile-rust" so the focused proof scripts can grep for the
  connect / subscribe / publish / inbound-event breadcrumbs.
* Best-effort Android-only URL sanity pass that rewrites
  ws://localhost:* / ws://127.0.0.1:* (host-loopback aliases that
  hit the emulator's own loopback instead of the host's 10.0.2.2
  alias) to ws://10.0.2.2:* on Android targets; emits an info-level
  breadcrumb so the rewrite is auditable.
* Add focused script
  apps/igloo-mobile/scripts/run-focus-android-signer-sign-readiness.sh
  with logcat capture, 5-step Maestro dance, intermediate 5/15/30/45/60 s
  snapshot loops, Stop -> Start cycle rearm assertion, and a static
  iOS-vs-Android snapshot matrix diff file.
* Add apps/igloo-mobile/rust/tests/signing_path_adapter.rs with 4
  assertions covering the new constructor, settle-window constants,
  default filter shape, and the Android loopback sanity pass-through
  on non-Android targets.

153 Rust tests pass on the state_machine_tests.rs binary; the wider
workspace grows to 251 tests across 10 binaries. Clippy and cargo fmt
are clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…shed validation worker

- Add signer_starting AtomicBool guard to prevent concurrent start_signer
  invocations (iOS Maestro double-tap race)
- Honor caller-supplied relay_url in FfiApp::onboard instead of always using
  package-embedded relays
- Canonicalize ws://localhost to ws://127.0.0.1 on iOS/macOS to avoid IPv6
  loopback resolution issues
- Add signer_runtime_recovery.rs test covering OnboardProfileMaterial
  round-trip, backward compatibility, readiness mapping, and status JSON shape
- Update focused proof scripts

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…fixes and focused validation scripts

- Android: expose CopyableKeyRow and copy-button testTags as resource-ids so
  Maestro can tap identity_group_pubkey_copy / identity_share_pubkey_copy.
- iOS: move CopyableKeyRow accessibilityIdentifier onto the value Text and add
  accessibilityValue so the 64-hex pubkey is reachable for clipboard verification.
- Add run-focus-android-signer-copy-and-ping.sh (VAL-SIGNER-017/018 on Android
  via OnboardConnect paste-back) and run-focus-ios-signer-copy-keys.sh.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Finish the reconstructed Igloo Mobile RMP mission with native iOS and Android coverage for onboarding, create/load/export, QR display, rotate share, backup publication, settings persistence, and focused validation helpers.

Review cleanup: diagnostics-gate the iOS test-inject path before it can start onboarding, keeping release builds and unflagged debug launches inert.

Validation: just doctor; cargo fmt --manifest-path rust/Cargo.toml --all --check; cargo test --manifest-path rust/Cargo.toml --workspace; cargo clippy --manifest-path rust/Cargo.toml --workspace --all-targets -- -D warnings; just ios-build; just android-full; ./gradlew :app:testDebugUnitTest; npm --prefix test run test:guards; git diff --check; Android emulator launch smoke; EVIDENCE_DIR=.tmp/... just focus-ios-export.
@AustinKelsay
AustinKelsay changed the base branch from master to dev June 29, 2026 14:32
@AustinKelsay
AustinKelsay merged commit 9bfac59 into dev Jul 5, 2026
4 of 6 checks passed
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