add Euclid 3.0 settings screens, tunnel flow, and audit pipeline skills (#1858)

* feat(webview-app): add Euclid 3.0 settings sub-screens

Add SecurityScreen, NotificationPreferencesScreen, and DevModeScreen
wrappers that import Euclid 3.0 components and wire them with React
Router navigation and bridge adapters. Update SettingsScreen menu items
to navigate to real routes instead of /coming-soon.

Requires @selfxyz/euclid-web to be published with the new screen exports
(SecurityScreen, NotificationPreferencesScreen, DevModeScreen) before
type-check will pass. See docs/superpowers/plans/ for full context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add settings integration plan and handover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: use euclid 1.2.0

* PoC tunnel flow

* updates

* update skills

---------

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Leszek Stachowski <leszek.stachowski@self.xyz>
This commit is contained in:
Justin Hernandez
2026-03-23 23:55:31 -07:00
committed by GitHub
parent 4338d08095
commit bd2e3c6738
37 changed files with 2135 additions and 193 deletions

View File

@@ -0,0 +1,93 @@
---
name: gaps-to-issues
description: Create Linear issues from a PR audit doc — one issue per PR bucket with acceptance criteria and linked audit findings.
disable-model-invocation: false
user-invocable: true
argument-hint: '[path-to-audit-doc]'
---
# Gaps to Issues
You take a PR audit document (produced by `/pr-audit`) and create Linear issues from its PR buckets.
This skill **stops at issue creation**. Use `/spec-from-audit` to generate specs for the created issues.
## Input
`$ARGUMENTS` — Path to the audit document (e.g., `docs/reviews/2026-03-23-branch-audit.md`). If not provided, look for the most recently modified file in `docs/reviews/`.
## Workflow
### Step 1: Read the Audit Document
Read the audit doc. Verify it has:
- A "PR Buckets" section with at least one bucket
- Each bucket has: name, estimated LOC, findings, files, acceptance criteria
If the audit doc is missing buckets or acceptance criteria, tell the user and stop.
### Step 2: Ask for Linear Context
Ask the user:
1. Which **Linear team** to create issues under (suggest the team from recent issues if detectable)
2. Which **Linear project** to add issues to (optional)
3. Whether to set **priority** per bucket or use a default
4. Whether to **consolidate** any buckets before creating issues
Wait for confirmation before proceeding.
### Step 3: Create Issues
For each bucket, create a Linear issue using `mcp__linear-server__save_issue`:
- **Title:** Bucket name (concise, under 70 characters)
- **Team:** From Step 2
- **Project:** From Step 2 (if provided)
- **Priority:** Based on the highest severity finding in the bucket:
- Contains Critical findings → Urgent (1)
- Contains High findings → High (2)
- Contains Medium findings → Medium (3)
- Contains only Low findings → Low (4)
- **Description:** Keep it lightweight — the spec (created later by `/spec-from-audit`) is the source of truth. The issue body should contain:
- One-sentence goal
- List of finding IDs from the audit (e.g., "Covers findings #1, #3, #7")
- Dependencies on other issues if any
- A note: "See attached spec document for full implementation plan."
- Do NOT duplicate scope, file lists, or acceptance criteria — that belongs in the spec
- **Links:** Add the PR URL if one exists
If the user asked to consolidate buckets, merge the relevant findings.
### Step 4: Update the Audit Document
Add a "Follow-up Issues" section to the top of the audit doc (below the header metadata) with a table of all created issues:
```markdown
## Follow-up Issues
| Issue | Priority | Title |
|-------|----------|-------|
| [SELF-NNNN](url) | Urgent | Bucket name |
| [SELF-NNNN](url) | High | Bucket name |
```
Update the audit doc status to: `Audit complete — issues created`
### Step 5: Present Results
Show the user:
1. Table of created issues with IDs, priorities, and URLs
2. Any buckets that were skipped or consolidated
3. Remind them: "Use `/spec-from-audit` to generate specs for these issues."
**Stop here.** Do not create specs.
## Important Notes
- Never run `git commit` or `git push`.
- Ask before creating — never create issues without user confirmation on team/project/priority.
- If a bucket seems too large (>2k LOC estimate), suggest splitting before creating.
- If buckets have dependencies between them, note this in the issue descriptions and use Linear's `blockedBy` field.

View File

@@ -0,0 +1,210 @@
---
name: pr-audit
description: Multi-agent PR review — component, integration, and routing analysis merged into a structured audit doc with severity and PR buckets.
disable-model-invocation: false
user-invocable: true
argument-hint: '[base-branch (default: dev)]'
---
# PR Audit
You run a multi-pass review of the current branch, pull in external PR feedback, and produce a structured audit document with findings grouped into PR-sized fix buckets.
This skill **stops at the audit doc**. It does not create issues or specs — use `/gaps-to-issues` and `/spec-from-audit` for those steps.
## Input
`$ARGUMENTS` — Optional base branch name (default: `dev`).
## Workflow
### Step 1: Gather the Diff
Use `$ARGUMENTS` if provided, otherwise default to `dev`.
Verify the base branch exists:
```bash
git rev-parse --verify <base-branch>
```
If it does not exist, ask the user which branch to diff against.
Gather the full diff and context:
```bash
git log <base-branch>..HEAD --oneline --no-decorate
git diff <base-branch>...HEAD --stat
git diff <base-branch>...HEAD
git diff <base-branch>...HEAD --name-only
git status -s
```
Read the full diff carefully. You will reference it throughout the review passes.
### Step 2: Multi-Agent Review (3 Parallel Passes)
Run all three review passes **in parallel** using the Agent tool. Each agent receives the diff context and reviews through a specific lens.
#### Pass A: Component-Level Review
For every new or changed screen/component file, check:
- Missing or incorrect imports (especially cross-package imports that don't resolve)
- Wrong or missing props vs the component's actual interface — read the component definition, don't guess
- Missing Lottie/animation assets or props that the component supports but aren't passed
- Placeholder logic left in production code (hardcoded values, no-op callbacks, `TODO`/`FIXME`)
- Missing error/loading/empty states
- Raw hex colors instead of design tokens
- Files exceeding 800 LOC
#### Pass B: Integration-Level Review
For the full set of changed files, check:
- Provider/contract flow violations — does data flow match what specs define?
- State propagation between screens — is state passed correctly through navigation, or lost on refresh/direct-entry?
- Persistence gaps — state that should survive reload but only lives in `useState`
- Event ordering issues — race conditions, guards that swallow later events
- Breaking changes to shared interfaces consumed by other packages
- Build-time vs runtime config that should be dynamic
#### Pass C: Route Wiring Review
For all navigation-related changes, check:
- Every `navigate()` / `push()` / `replace()` target has a corresponding route definition
- Every new route definition is reachable from at least one navigation call
- Orphaned routes — defined but unreachable
- Screens that receive `location.state` but have no guard for direct-entry (missing state)
- Deep link configuration consistency (if applicable)
### Step 3: Pull in PR Feedback
Check if a PR exists for this branch:
```bash
gh pr view --json number,url 2>/dev/null
```
If a PR exists, fetch external review comments:
```bash
gh api repos/{owner}/{repo}/pulls/{number}/comments --paginate
gh api repos/{owner}/{repo}/pulls/{number}/reviews --paginate
gh api repos/{owner}/{repo}/issues/{number}/comments --paginate
```
Extract actionable items from CodeRabbit, Codex, and human reviewers. Ignore resolved conversations and pure acknowledgment comments.
If no PR exists, skip this step.
### Step 4: Merge and Deduplicate
Combine findings from all three passes and PR feedback. Deduplicate — if Pass A and a CodeRabbit comment flag the same issue, keep one entry and note both sources.
Assign severity:
| Severity | Criteria |
|----------|----------|
| **Critical** | Build-breaking, crashes, data loss, security holes, blocked user flows |
| **High** | Incorrect behavior visible to users, contract violations, missing error handling on critical paths |
| **Medium** | Missing states, prop mismatches, design token violations, placeholder logic |
| **Low** | Style, minor code quality, non-blocking TODOs |
### Step 5: Group into PR-Sized Buckets
Group findings into fix buckets. Each bucket:
- Targets ≤2,000 LOC changed
- Groups related findings touching the same files or logical area
- Is independently mergeable (note dependencies if unavoidable)
- Has a clear name, estimated LOC, list of files, and acceptance criteria
### Step 6: Write the Audit Document
Determine the branch slug from the current branch name (strip `feat/`, `fix/`, etc., replace `/` with `-`).
Create the audit document at:
```
docs/reviews/YYYY-MM-DD-<branch-slug>-audit.md
```
Create `docs/reviews/` if it does not exist.
Structure:
```markdown
# PR Audit — <branch-name>
> Date: YYYY-MM-DD
> Branch: `<branch-name>`
> Base: `<base-branch>`
> PR: #NNN (if exists)
> Reviewers: Claude Code (component, integration, routing passes), [external reviewers]
> Status: Audit complete — ready for review
## Summary
[2-3 sentences describing what the branch does and the overall health assessment.]
## Findings
### Critical
- [ ] [finding] — `path/to/file.ts:NN`
- **Source:** [Pass A | Pass B | Pass C | CodeRabbit | reviewer]
- **Fix:** [concrete action]
### High
- [ ] ...
### Medium
- [ ] ...
### Low
- [ ] ...
## PR Buckets
### Bucket 1: [name]
- **Estimated LOC:** ~NNN
- **Findings:** #1, #3, #7
- **Files:** [list]
- **Dependencies:** [none | Bucket N must land first]
- **Acceptance criteria:**
- [ ] [criterion]
### Bucket 2: [name]
...
## What Works Well
[List things the branch does correctly — not just problems.]
```
### Step 7: Present to User
Show the user:
1. Total finding count by severity
2. Each bucket with name, estimated LOC, and findings covered
3. Any findings that didn't fit cleanly into a bucket
Tell the user:
> "Audit complete. Review the findings and buckets above. When ready, use `/gaps-to-issues` to create Linear issues from these buckets, then `/spec-from-audit` to generate specs."
**Stop here.** Do not create issues or specs.
## Important Notes
- Never run `git commit` or `git push`. Stage the audit doc but stop there.
- Read the full diff — do not summarize from file names alone.
- Findings must be specific and actionable with file paths and line numbers.
- When estimating LOC for buckets, be conservative.
- If the diff is very large (>10k LOC), warn the user and suggest focusing on critical/high only.

View File

@@ -0,0 +1,136 @@
---
name: spec-from-audit
description: Generate one Linear spec document per issue — agent-executable implementation plans with file paths, validation commands, and acceptance criteria.
disable-model-invocation: false
user-invocable: true
argument-hint: '[issue IDs or path-to-audit-doc]'
---
# Spec from Audit
You take Linear issues (created by `/gaps-to-issues`) and generate one spec per issue as a Linear document. Each spec is an agent-executable implementation plan — a new Claude Code session with no prior context should be able to pick up the spec and produce a correct PR.
## Input
`$ARGUMENTS` — Either:
- A list of Linear issue IDs (e.g., `SELF-2357 SELF-2358 SELF-2359`)
- A path to an audit doc that has a "Follow-up Issues" section
If not provided, look for the most recently modified audit doc in `docs/reviews/` and extract issue IDs from it.
## Workflow
### Step 1: Gather Issue Context
For each issue ID, fetch the issue details:
- Use `mcp__linear-server__get_issue` to read the title, description, and acceptance criteria
- If an audit doc path is available, read the full audit doc for additional context on findings
### Step 2: Read the Codebase
For each issue, read the files referenced in its description/scope. You need to understand the **current state** of the code to write accurate specs with line numbers.
Do not guess file contents — read them. Specs with wrong line numbers or stale code references are worse than no spec.
### Step 3: Generate Specs
For each issue, create a spec as a Linear document using `mcp__linear-server__create_document`.
Link the document to the **issue** (not the project).
**Title format:** `SPEC: <issue title>`
**Spec structure:**
```markdown
## Overview
You are [doing what] in [which area]. [1-2 sentences on why this matters.]
## Preconditions
- [What must be true before starting — dependencies on other PRs, packages published, etc.]
## [Problem 1 / Area 1]: [descriptive name]
**File:** `path/to/file.ts`
[Description of the current problem with exact line numbers.]
### Fix
[Step-by-step instructions. Be explicit about what to change and why.]
## [Problem 2 / Area 2]: [descriptive name]
...
## Files to modify
- `path/to/file.ts` — [what changes]
- `path/to/other.ts` — [what changes]
## Files NOT to modify
- `path/to/untouched/` — [why]
## Dependencies
- [Other issues/PRs that must land first, if any]
## Validation
```bash
[relevant validation commands from the repo]
```
## Definition of Done
- [ ] [acceptance criterion from the issue]
- [ ] [acceptance criterion]
- [ ] All validation commands pass
```
### Spec-Writing Rules
The cardinal rule: **if two reasonable engineers could implement different solutions from the same spec, the spec is too open.** Specs must contain decisions, not options.
Follow these strictly:
1. **Decisions, not options** — "Use local wrappers" not "Consider adding to Euclid or using local wrappers." Every ambiguous implementation choice must be resolved in the spec. If you genuinely can't decide, flag it as a blocker and ask the user — don't embed it as an option.
2. **Second person** — "You are fixing...", "You will modify..."
3. **Exact file paths with line numbers** — `src/utils/sumsubProvider.ts:118`, not "the provider file"
4. **Current code, not stale references** — you read the files in Step 2, use what you actually saw
5. **Explicit constraints** — "You will NOT modify..." sections prevent scope creep
6. **Required vs optional** — mark every item. Don't let agents infer priority.
7. **Validation command** — agents will run it. If it's not there, they'll skip validation.
8. **One spec = one PR ≤2k LOC** — if a spec feels like it would produce >2k LOC, flag it and suggest splitting
9. **Self-contained** — the spec must include enough context to execute without reading other specs or the full audit doc
Every spec should follow this structure:
1. State the goal in one sentence
2. List decisions already made
3. Define scope and out-of-scope
4. Name the files to modify
5. Define done criteria in behavior terms
### Step 4: Present Results
Show the user:
1. Table of created specs with issue IDs and document URLs
2. Any issues that were too complex and need manual spec-writing
3. Any issues where codebase reading revealed the problem is already fixed or different than described
**Stop here.**
## Important Notes
- Never run `git commit` or `git push`.
- Always read the actual files before writing specs — stale line numbers are actively harmful.
- If an issue references files that don't exist, flag it rather than guessing.
- If you discover the issue description is wrong (e.g., the code was already fixed), note this and ask the user whether to still create the spec or update the issue.
- Specs are for agents, not humans. Write them as precise instructions, not explanatory documents.
- The spec is the source of truth, not the issue body. Issue bodies are lightweight pointers — the spec must be fully self-contained. Do not assume the agent has read the issue description.
- Link specs to issues (not projects). Use `mcp__linear-server__create_document` with the `issue` parameter.

View File

@@ -46,57 +46,54 @@ nvm use && corepack enable && yarn install
## Specs & Planning
**Every feature — even minor ones — uses the spec system.** Before implementing, read the relevant specs, write a plan to disk, then execute. No exceptions. A plan that only exists in session memory is a plan that will be lost.
**Every feature — even minor ones — needs a spec.** Specs live in **Linear** as documents attached to issues. Do not create spec files in the `specs/` folder — that folder is deprecated and being migrated to Linear.
### Spec System (`specs/`)
### Where Specs Live
| File | Purpose | When to Read |
| ------------------------------------------------ | ------------------------------------------- | ------------------------ |
| [Specs README](./specs/README.md) | Table of contents, reading order | First. Always. |
| [Templates](./specs/framework/TEMPLATES.md) | Copy-paste templates for all three tiers | When creating a new spec |
| [SDK Overview](./specs/projects/sdk/OVERVIEW.md) | Architecture, bridge protocol, module table | For system-level context |
Workstream specs live in `specs/projects/sdk/workstreams/*/` with `SPEC.md` (living implementation details).
### Spec-Reading Protocol (for chunk execution)
To execute a chunk:
1. Read `specs/projects/sdk/INDEX.md` — find your workstream
2. Read the workstream `SPEC.md` — find your chunk
3. If you need architecture context, read the project `OVERVIEW.md`
That's it. Do not read framework docs unless you are writing a new spec.
- **Specs → Linear documents**, attached to the relevant Linear issue (not the project)
- **Architecture context → `specs/projects/sdk/OVERVIEW.md`** (read-only reference, still in repo)
- **Audit docs → `docs/reviews/`** (PR audit findings, kept in repo for git history)
- **`specs/` folder** — deprecated. Existing specs are being migrated to Linear. Do not create new files here.
### Planning Protocol
1. **Read** the relevant workstream specs and this file's Key Rules — understand the current state and constraints
2. **Write a plan to disk** — use the appropriate tier from `specs/framework/TEMPLATES.md`:
- **Large features / new workstreams:** Create a full implementation spec (`specs/projects/sdk/workstreams/<scope>/SPEC.md`)
- **Medium features / multi-chunk work:** Create a plan file in `workstreams/<scope>/plans/` named `<BACKLOG-ID>-<slug>.md` and link it from the backlog in the relevant `SPEC.md`
- **Small features / single-chunk fixes:** Create a minimal plan file in `workstreams/<scope>/plans/` named `<BACKLOG-ID>-<slug>.md` or add the chunk to an existing active plan
3. **Include in every plan:** scope of work, files modified, I/O examples, validation command, definition of done
4. **Then implement** — update chunk status as you complete work
5. **After completion:** Mark chunks done in SPEC.md status tables. Review status checklists at session start — if something is marked "Done" that isn't, or "Pending" that's in progress, fix it first.
1. **Read** this file's Key Rules and any relevant Linear issue/spec — understand the current state and constraints
2. **Create a Linear issue** if one doesn't exist — include scope, files modified, acceptance criteria
3. **Create a spec as a Linear document** attached to the issue — this is the execution plan
4. **Then implement** — one spec = one PR ≤2k LOC
5. **After completion:** Update the Linear issue status. Close when done.
### Spec-Writing Guidelines
When writing specs, follow these principles so they work as AI agent prompts:
Specs are agent-executable prompts. A new Claude Code session with no prior context must be able to pick up the spec and produce a correct PR.
- **Use second person.** "You are making X portable" not "X should be made portable."
- **Make decisions, not options.** "Use local wrappers" not "Consider adding to Euclid or using local wrappers." Agents can't choose between approaches — tell them which one.
- **Use second person.** "You are fixing X" not "X should be fixed."
- **Be explicit about constraints.** "You will NOT modify..." not just "Focus on..."
- **Provide exact file paths with line numbers.** `src/proving/provingMachine.ts:543` not "the proving machine file."
- **Provide exact file paths with line numbers.** `src/utils/sumsubProvider.ts:118` not "the provider file."
- **State the validation command.** Agents will run it. If it's not there, they'll skip validation.
- **One chunk = one self-contained prompt.** The chunk must include enough context to execute without reading the full spec.
- **One PR = one plan file.** A plan file is the execution handoff. It must be self-contained enough that a new agent can pick it up after session loss.
- **Use `--remote` for M and L chunks.** Medium and large chunks benefit from `claude --remote` so work continues in the background.
- **One spec = one PR ≤2k LOC.** If a spec would produce >2k LOC, split it.
- **Mark items as required vs optional.** Don't let agents infer priority.
- **Include out-of-scope sections.** These are as important as in-scope sections for preventing drift.
- **Use `--remote` for medium+ work.** Medium and large specs benefit from `claude --remote` so work continues in the background.
### Why Even Minor Features
### Audit Pipeline Skills
Three Claude Code skills automate the review-to-implementation pipeline:
1. **`/pr-audit`** — Multi-agent review (component + integration + routing), produces audit doc in `docs/reviews/`
2. **`/gaps-to-issues`** — Creates Linear issues from audit PR buckets
3. **`/spec-from-audit`** — Generates agent-executable specs as Linear documents, one per issue
Run them in sequence with review pauses between each step.
### Why Specs
- Prevents scope creep — writing "files NOT modified" forces focus
- Survives session loss — API errors, context overflow, `/clear` won't destroy the plan
- Enables parallel work — multiple agents can pick up chunks from the same plan
- Survives session loss — specs live in Linear, not session memory
- Enables parallel work — multiple agents can pick up specs from the same project
- Creates audit trail — what was planned vs what was built
- Enables cross-tool review — anyone with Linear access can review specs, not just GitHub users
## Validation Commands

View File

@@ -290,5 +290,5 @@ See `.cursor/rules/test-memory-optimization.mdc` for comprehensive guidelines, e
The Self Wallet app serves as a **test environment** for the SDK refactor. For SDK architecture context:
- **[SDK Overview](../specs/projects/sdk/OVERVIEW.md)** — System architecture, bridge protocol, decision matrix
- **[SDK Project Index](../specs/projects/sdk/INDEX.md)** — Workstream links and entry point
- **[SDK Overview](../specs/projects/sdk/OVERVIEW.md)** — System architecture, bridge protocol, decision matrix (read-only reference)
- **Implementation specs** — Live in Linear as documents attached to issues. Check the relevant Linear issue for the current spec. Do not create spec files in `specs/` — that folder is deprecated.

View File

@@ -0,0 +1,160 @@
# PR Audit — feat/euclid-settings-screens-rd2
> Date: 2026-03-23
> Branch: `feat/euclid-settings-screens-rd2`
> Base: `dev`
> PR: #1858
> Reviewers: Claude Code, Codex
> Status: Ship as-is with follow-up issues for known gaps
## Follow-up Issues
| Issue | Priority | Title |
| ------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------ |
| [SELF-2357](https://linear.app/selfprotocol/issue/SELF-2357) | Urgent | Euclid migration — complete euclid-web → euclid and validate API compatibility |
| [SELF-2358](https://linear.app/selfprotocol/issue/SELF-2358) | High | Sumsub / WV-05 contract compliance |
| [SELF-2359](https://linear.app/selfprotocol/issue/SELF-2359) | High | Tunnel + proving flow data propagation and UI contract fixes |
| [SELF-2360](https://linear.app/selfprotocol/issue/SELF-2360) | Medium | Settings persistence, test coverage, and doc/spec cleanup |
Each issue has a spec attached as a Linear document.
## Summary
This PR adds Euclid 3.0 settings sub-screens (Security, Notifications, Dev Mode), a PoC tunnel flow, the `euclid-web``euclid` migration, and a Sumsub Web SDK integration in the provider launch flow. It ships with known gaps that are tracked as follow-up issues.
## Merged Findings
Findings from both Claude Code and Codex reviews, deduplicated and grouped by follow-up issue.
### Issue 1: Euclid migration — complete `euclid-web` → `euclid` and validate exports
**Severity:** Critical (build-breaking)
- 3 settings screen components (`SecurityScreen`, `NotificationPreferencesScreen`, `DevModeScreen`) are imported from `@selfxyz/euclid` but not yet exported from the package
- `LaunchTour1Screen``LaunchTour4Screen` don't exist — Euclid only exports a single `TourScreen`
- `ProofGenerationScreen` doesn't exist — only a `ProofGeneration` component (not a screen wrapper)
- ~~`ProviderResultScreen` still imports from `@selfxyz/euclid-web`~~ — **fixed** (migrated to `@selfxyz/euclid`)
- ~~`ProviderLaunchScreen` still imports from `@selfxyz/euclid-web`~~ — **fixed** (migrated to `@selfxyz/euclid`, removed unused `BodyText`)
- Multiple screens migrated from `euclid-web` to `euclid` — blast radius is package-wide, not just settings
- `euclid-web` is being retired; all imports should use `@selfxyz/euclid`
- Settings handover doc (`docs/superpowers/plans/2026-03-22-settings-handover.md (legacy location)`) says the app still imports `euclid-web` — stale
**Acceptance criteria:**
- All imports use `@selfxyz/euclid`, zero references to `euclid-web`
- All imported screen components exist in Euclid's exports
- `yarn workspace @selfxyz/webview-app build` passes
### Issue 2: Sumsub / WV-05 contract compliance
**Severity:** High (incorrect behavior)
- `normalizeSumsubStatus()` marks `reviewAnswer === 'GREEN'` as `status: 'success'` without requiring `attestation` — breaks the KYC contract
- `onApplicantSubmitted` emits `status: 'partial'` through the `onComplete` callback (not `onError`), so `ProviderResultScreen` treats it as success and routes to `/proving`
- `emitOnce` guard means if `onApplicantSubmitted` fires before `applicantReviewComplete`, the actual terminal status is silently dropped
- `ProvingScreen` fabricates a successful `VerificationResult` without consuming any provider attestation payload
- `teeUrl` not parsed from launch URL/query params — `fetchSumsubAccessToken()` uses `VITE_SUMSUB_TEE_URL` or hardcoded default
- `fetchSumsubAccessToken` signature doesn't accept `teeUrl` as a parameter — even after parsing, plumbing is missing
- Token refresh callback in `launchSumsubWebSdk` (line 167) also calls `fetchSumsubAccessToken()` with no args — refresh hits hardcoded URL too
- `sumsub-websdk.d.ts` custom type declaration added — unclear if it matches actual SDK API or is a stub
- WV-05 plan says "code complete, needs testing" but `teeUrl` isn't parsed, service file structure wasn't created, and normalization doesn't match the contract
- Invalid step value: `TunnelProvingScreen` passes `step="generatingProof"` — valid values are `"registeringId" | "generatingProof1" | "awaitingVerification" | "finishingUp"`
**Acceptance criteria:**
- `normalizeSumsubStatus` requires attestation for `success`
- `partial` status blocked from entering proving flow
- `teeUrl` parsed from query params, threaded into token fetch and refresh
- WV-05 plan status downgraded to "partial implementation"
### Issue 3: Tunnel flow correctness and state propagation
**Severity:** High (data loss / broken UX)
- Selected ID type discarded in `TunnelIDTypeScreen.onIDTypeSelect` — ignores the param, always navigates to `/tunnel/proof/receipt`
- State passed via `location.state` is fragile — lost on refresh or direct navigation, no fallback or route guard
- `TunnelProofReceiptScreen` passes invalid `documentType="passport"` prop to `ProofRequestScreen` (not a valid prop)
- `TunnelProofReceiptScreen` mock items lack icons and `onInfoPress` callbacks
- `KycMockScreen` is raw HTML divs with inline styles instead of Euclid components
- `TunnelResultScreen` missing `animationSource` (Lottie) — uses icon only
- `Date.now()` in `TunnelProofReceiptScreen` creates new timestamp on every render
- Main proving flow (`ProvingScreen.tsx`) hardcodes `documentType='passport'` — affects primary flow, not just tunnel
**Acceptance criteria:**
- ID type selection persisted and forwarded through the tunnel chain
- Screens handle missing state gracefully (redirect or error)
- Correct Euclid component props used throughout
### Issue 4: Settings persistence and bridge-backed actions
**Severity:** Medium (non-functional features)
- `SecurityScreen` backup state is hardcoded `false` — should query actual state from bridge/storage
- `SecurityScreen` handlers for backup, recovery phrase, restore all navigate to `/coming-soon`
- `NotificationPreferencesScreen` toggles are local `useState` — lost on reload, never persisted
- `DevModeScreen` mock config is local state — lost on reload
- `DevModeScreen` "Generate mock document" fires analytics but doesn't create or store anything
- `SettingsScreen` "Manage Documents", "Get support", "Share Self" all navigate to `/coming-soon`
**Acceptance criteria:**
- Settings state backed by bridge/storage where applicable
- Placeholder routes documented as intentional
### Issue 5: Test coverage
**Severity:** Medium
- No tests for Sumsub normalization/result mapping (`normalizeSumsubStatus`, `buildProviderResult`)
- No route tests for tunnel flow progression
- No smoke tests for settings wrapper navigation and action wiring
- No tests for provider launch/result flow
**Acceptance criteria:**
- Unit tests for Sumsub normalization logic
- Route/navigation tests for tunnel flow
- Smoke tests for settings screens
### Issue 6: Doc/spec drift
**Severity:** Low
- Settings handover doc (`docs/superpowers/plans/2026-03-22-settings-handover.md (legacy location)`) is stale — references `euclid-web`, understates scope
- Settings integration plan (`docs/superpowers/plans/2026-03-22-settings-screen-integration.md (legacy location)`) scoped to settings only but branch includes tunnel/provider/migration
- WV-05 plan status overstated
- Orphaned route: `/onboarding/confirm` defined in App.tsx but never navigated to
**Acceptance criteria:**
- Docs reflect actual branch state
- WV-05 status corrected
- Orphaned route removed or documented
## Actionable PR feedback (CodeRabbit + Codex connector)
Extracted from PR #1858 inline comments. Items already covered above are not repeated.
### Build-breaking (addressed in this branch)
- **`@selfxyz/euclid-web` removed from `package.json` but still imported** — `ProviderLaunchScreen` and `ProviderResultScreen` still referenced it. Both migrated to `@selfxyz/euclid` in this branch. (CodeRabbit P1, Codex connector P1)
### Actionable (not yet addressed)
- **`TunnelIDTypeScreen`: pass selected `idType` to next screen** — `onIDTypeSelect` ignores the param. Suggested fix: `navigate('/tunnel/proof/receipt', { state: { documentType: idType.id } })`. (CodeRabbit, mapped to Issue 3)
- **`TunnelProofReceiptScreen`: read document type from route state** — Currently hardcodes `documentType="passport"`. Should read from `location.state` with fallback. (CodeRabbit, mapped to Issue 3)
- **`NotificationPreferencesScreen`: persist toggles** — Use `storage` adapter from `useSelfClient()` to save/load preferences. (CodeRabbit, mapped to Issue 4)
- **`DevModeScreen`: mock document generation is a no-op** — Should either persist the mock or show a toast indicating the feature isn't functional yet. (CodeRabbit, mapped to Issue 4)
### Nitpicks (low priority)
- **`NotificationPreferencesScreen`: memoize toggle handlers** — `toggles` array recreated every render. `useMemo` with `toggleValues` as dependency would be cleaner.
## What works well
- Route wiring is clean — every route has a file, every `navigate()` targets a defined route
- Settings menu restructure (App settings / Support & feedback / Developer tools) is well-organized
- Sumsub SDK lifecycle management (mount/destroy/abort) is properly handled
- Analytics events are consistently tracked across all screens
- `emitOnce` pattern prevents duplicate callbacks (though it has the silenced-review-status side effect)

View File

@@ -0,0 +1,102 @@
# Euclid 3.0 Settings Integration — Handover
**Branch:** `feat/euclid-settings-screens` (based off `origin/main`)
**Commit:** `feat(webview-app): add Euclid 3.0 settings sub-screens`
## What Was Done
Three new wrapper screens were added to `packages/webview-app/src/screens/account/`:
| File | Euclid Component | Route |
| ----------------------------------- | ------------------------------- | ------------------------- |
| `SecurityScreen.tsx` | `SecurityScreen` | `/settings/security` |
| `NotificationPreferencesScreen.tsx` | `NotificationPreferencesScreen` | `/settings/notifications` |
| `DevModeScreen.tsx` | `DevModeScreen` | `/settings/dev-mode` |
Routes added to `App.tsx`. `SettingsScreen.tsx` updated with three sections (App settings, Support & feedback, Developer tools) — "Security", "Notifications", and "Dev mode" now navigate to real screens instead of `/coming-soon`.
Each wrapper follows the existing pattern (see `CountryPickerScreen.tsx`): import Euclid component, wire with `useNavigate()` + `useSelfClient()` bridge adapters, manage local UI state.
## Blocker: Euclid Package Publish
The installed `@selfxyz/euclid-web@1.0.2` does **not** export `SecurityScreen`, `NotificationPreferencesScreen`, or `DevModeScreen`. These screens exist on `origin/main` of the **euclid repo** (`/Users/evinova-self/Documents/euclid`) but haven't been published to npm yet.
**To unblock, you need to publish a new version of `@selfxyz/euclid`** (note: package was renamed from `euclid-web` to `euclid` on euclid's `origin/main`).
Steps:
1. In the euclid repo, check out `origin/main` and verify the screens export: `git show origin/main:packages/euclid/src/screens/index.ts`
2. Bump the version and publish (the euclid repo has automated publishing via PR merge of version bump PRs)
3. In the self repo, update `packages/webview-app/package.json` to use the new version (and rename dependency from `@selfxyz/euclid-web` to `@selfxyz/euclid` if the package name changed)
4. Run `yarn install` to pull the new package
5. Run `yarn workspace @selfxyz/webview-app exec tsc --noEmit` to verify type-check passes
### Package Rename Note
The euclid repo renamed the web package from `@selfxyz/euclid-web` to `@selfxyz/euclid`. The webview-app still imports from `@selfxyz/euclid-web`. When updating the dependency, you'll also need to update all import paths across the webview-app screens:
```
- import { ... } from '@selfxyz/euclid-web';
+ import { ... } from '@selfxyz/euclid';
```
## Pre-Existing Type Errors
These errors exist on `origin/main` independent of our changes:
- `BridgeProvider.tsx``browserHost` not in `WebViewBridgeOptions`
- `SelfClientProvider.tsx``lifecycle.dismiss()` argument mismatch
- `ConfirmIdentificationScreen.tsx`, `ProvingScreen.tsx`, `VerificationResultScreen.tsx``VerificationResult` type mismatch
- `ProviderLaunchScreen.tsx``lifecycle.dismiss()` argument mismatch
## What Still Uses `/coming-soon`
These items in SettingsScreen still navigate to `/coming-soon` (no Euclid screen exists yet):
- **"Manage Documents"** — needs a document management screen
- **"Get support"** — needs external link / bridge intent
- **"Share Self"** — needs native share sheet via bridge
Within the sub-screens, these actions are also stubbed:
- **SecurityScreen**: "Backup your account", "Reveal recovery phrase", "Restore an account" → all go to `/coming-soon` (need native bridge integration for biometric auth, iCloud backup, wallet restore)
- **NotificationPreferencesScreen**: Toggle state is local-only (needs bridge storage persistence)
- **DevModeScreen**: "Generate mock document" tracks analytics and navigates home but doesn't actually create a mock document yet
## Navigation Flow
```
/settings (SettingsViewScreen)
├── Manage Documents → /coming-soon
├── Security → /settings/security ✅ NEW
│ ├── Backup your account → /coming-soon
│ ├── Reveal recovery phrase → /coming-soon
│ ├── Restore an account → /coming-soon
│ └── Disable backups → local dialogue toggle
├── Notifications → /settings/notifications ✅ NEW
│ └── Toggles → local state only
├── Get support → /coming-soon
├── Share Self → /coming-soon
├── Dev mode → /settings/dev-mode ✅ NEW
│ ├── Steppers/toggles → local state
│ ├── Generate mock document → analytics + navigate home
│ └── Reset all values → reset state
└── Close Self → lifecycle.dismiss()
```
## Validation
After unblocking the euclid dependency:
```bash
yarn workspace @selfxyz/webview-app exec tsc --noEmit # type-check
yarn workspace @selfxyz/webview-app build # Vite production build
yarn lint # lint
```
## Related Resources
- **Implementation plan:** `docs/superpowers/plans/2026-03-22-settings-screen-integration.md`
- **Euclid screen source:** `euclid repo origin/main:packages/euclid/src/screens/settings/`
- **Euclid storybook stories:** `euclid repo origin/main:packages/storybook/stories/*Screen.stories.tsx`
- **Linear tickets:** SELF-2223 (Settings), SELF-2311 (Update core app)

View File

@@ -0,0 +1,667 @@
# Settings Screen Integration — Import Euclid 3.0 Screens into WebView App
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the five `/coming-soon` navigations in the webview-app's SettingsScreen with real Euclid 3.0 sub-screens (Security, Notifications, Dev Mode), plus wire the remaining menu items to appropriate bridge actions.
**Architecture:** Each settings sub-screen gets a thin wrapper in `packages/webview-app/src/screens/account/` that imports the Euclid component from `@selfxyz/euclid-web`, wires it with `useSelfClient()` bridge adapters and `useNavigate()` from React Router, and manages local UI state (e.g. dialogue visibility, toggle values). Routes are added to `App.tsx`. The existing wrapper pattern (see `CountryPickerScreen.tsx`, `ComingSoonScreen.tsx`) is followed exactly.
**Tech Stack:** React, React Router, `@selfxyz/euclid-web` (Euclid 3.0 component library), `@selfxyz/webview-bridge` (bridge adapters)
**Existing pattern to follow:**
```
// webview-app wrapper screen pattern:
import { EuclidScreen } from '@selfxyz/euclid-web';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { useNavigate } from 'react-router-dom';
export const WrapperScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic, lifecycle } = useSelfClient();
// Wire Euclid props to bridge adapters + React Router
return <EuclidScreen {...wiredProps} />;
};
```
---
## File Map
| Action | File | Responsibility |
| ------ | ---------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Modify | `packages/webview-app/src/App.tsx` | Add 3 new routes |
| Modify | `packages/webview-app/src/screens/account/SettingsScreen.tsx` | Replace `/coming-soon` navigations with real routes + bridge actions |
| Create | `packages/webview-app/src/screens/account/SecurityScreen.tsx` | Wrapper for Euclid `SecurityScreen` |
| Create | `packages/webview-app/src/screens/account/NotificationPreferencesScreen.tsx` | Wrapper for Euclid `NotificationPreferencesScreen` |
| Create | `packages/webview-app/src/screens/account/DevModeScreen.tsx` | Wrapper for Euclid `DevModeScreen` |
---
## Task 1: SecurityScreen wrapper
The most important sub-screen. Wires backup state, recovery phrase, and restore actions through the bridge.
**Files:**
- Create: `packages/webview-app/src/screens/account/SecurityScreen.tsx`
- [ ] **Step 1: Create SecurityScreen wrapper**
```tsx
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
SecurityScreen as EuclidSecurityScreen,
LeftArrowIcon,
CloudKeyIcon,
LockIcon,
ZapShieldIcon,
} from '@selfxyz/euclid-web';
import { useSelfClient } from '../../providers/SelfClientProvider';
export const SecurityScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic, storage } = useSelfClient();
const [isBackupEnabled, setIsBackupEnabled] = useState(false);
const [showDisableDialogue, setShowDisableDialogue] = useState(false);
// TODO: Read actual backup state from bridge storage on mount
// useEffect(() => { storage.get('backup_enabled').then(...) }, []);
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const onBackupAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_backup_account_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onRevealRecoveryPhrase = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_reveal_phrase_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onRestoreAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_restore_account_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onDisableBackups = useCallback(() => {
haptic.trigger('warning');
setShowDisableDialogue(true);
}, [haptic]);
const onDisableICloudBackups = useCallback(() => {
haptic.trigger('warning');
analytics.trackEvent('security_backups_disabled');
setIsBackupEnabled(false);
setShowDisableDialogue(false);
// TODO: Persist via bridge storage
}, [haptic, analytics]);
const onDismissDialogue = useCallback(() => {
haptic.trigger('selection');
setShowDisableDialogue(false);
}, [haptic]);
return (
<EuclidSecurityScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
cloudKeyIcon={CloudKeyIcon}
lockIcon={LockIcon}
zapShieldIcon={ZapShieldIcon}
isBackupEnabled={isBackupEnabled}
onBack={onBack}
onBackupAccount={onBackupAccount}
onRevealRecoveryPhrase={onRevealRecoveryPhrase}
onRestoreAccount={onRestoreAccount}
onDisableBackups={onDisableBackups}
showDisableDialogue={showDisableDialogue}
onDisableICloudBackups={onDisableICloudBackups}
onDismissDialogue={onDismissDialogue}
/>
);
};
```
- [ ] **Step 2: Verify type-check passes**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: No errors related to SecurityScreen
- [ ] **Step 3: Commit**
```bash
git add packages/webview-app/src/screens/account/SecurityScreen.tsx
git commit -m "feat(webview-app): add SecurityScreen wrapper for Euclid 3.0"
```
---
## Task 2: NotificationPreferencesScreen wrapper
Manages toggle state locally (persisted via bridge storage in future).
**Files:**
- Create: `packages/webview-app/src/screens/account/NotificationPreferencesScreen.tsx`
- [ ] **Step 1: Create NotificationPreferencesScreen wrapper**
```tsx
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
NotificationPreferencesScreen as EuclidNotificationPreferencesScreen,
LeftArrowIcon,
} from '@selfxyz/euclid-web';
import { useSelfClient } from '../../providers/SelfClientProvider';
const defaultToggles = [
{
key: 'self',
label: 'Allow Self notifications',
description: 'App updates and more',
},
{
key: 'nova',
label: 'Allow Nova notifications',
description: 'Never miss a mission',
},
{
key: 'points',
label: 'Allow Self Points notifications',
description: 'Points and rewards',
},
{
key: 'id_status',
label: 'Allow ID status notifications',
description: 'Document verification updates',
},
];
export const NotificationPreferencesScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [toggleValues, setToggleValues] = useState<Record<string, boolean>>({
self: true,
nova: true,
points: true,
id_status: false,
});
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const toggles = defaultToggles.map(t => ({
label: t.label,
description: t.description,
value: toggleValues[t.key] ?? false,
onToggleChange: (value: boolean) => {
haptic.trigger('selection');
analytics.trackEvent('notification_toggle_changed', {
key: t.key,
value,
});
setToggleValues(prev => ({ ...prev, [t.key]: value }));
},
}));
return (
<EuclidNotificationPreferencesScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
onBack={onBack}
toggles={toggles}
/>
);
};
```
- [ ] **Step 2: Verify type-check passes**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: No errors related to NotificationPreferencesScreen
- [ ] **Step 3: Commit**
```bash
git add packages/webview-app/src/screens/account/NotificationPreferencesScreen.tsx
git commit -m "feat(webview-app): add NotificationPreferencesScreen wrapper for Euclid 3.0"
```
---
## Task 3: DevModeScreen wrapper
Manages mock document generation state. More complex — has steppers, dropdowns, toggle, and IDCard display.
**Files:**
- Create: `packages/webview-app/src/screens/account/DevModeScreen.tsx`
- [ ] **Step 1: Create DevModeScreen wrapper**
```tsx
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
DevModeScreen as EuclidDevModeScreen,
LeftArrowIcon,
} from '@selfxyz/euclid-web';
import type { IDCardProps } from '@selfxyz/euclid-web';
import { useSelfClient } from '../../providers/SelfClientProvider';
const ageOptions = ['18 or older', '21 or older', '25 or older', '30 or older'];
const expiryOptions = ['1 year', '2 years', '5 years', '10 years'];
export const DevModeScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [documentType, setDocumentType] = useState('passport');
const [nationality, setNationality] = useState('united states of america');
const [ageIndex, setAgeIndex] = useState(1);
const [expiryIndex, setExpiryIndex] = useState(2);
const [ofacCheck, setOfacCheck] = useState(true);
const idCard: IDCardProps = {
variant: 'dev-passport',
title: 'Developer Passport',
subtitle: 'Digital credential for developers',
};
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const onResetAllValues = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('dev_mode_reset');
setDocumentType('passport');
setNationality('united states of america');
setAgeIndex(1);
setExpiryIndex(2);
setOfacCheck(true);
}, [haptic, analytics]);
const onGenerateMockDocument = useCallback(() => {
haptic.trigger('success');
analytics.trackEvent('dev_mode_generate_mock', {
documentType,
nationality,
age: ageOptions[ageIndex],
expiresIn: expiryOptions[expiryIndex],
ofacCheck,
});
navigate('/');
}, [
navigate,
haptic,
analytics,
documentType,
nationality,
ageIndex,
expiryIndex,
ofacCheck,
]);
return (
<EuclidDevModeScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
onBack={onBack}
idCard={idCard}
documentType={documentType}
onDocumentTypePress={() => {
setDocumentType(prev => (prev === 'passport' ? 'id_card' : 'passport'));
}}
nationality={nationality}
onNationalityPress={() => {
setNationality(prev =>
prev === 'united states of america'
? 'germany'
: 'united states of america',
);
}}
age={ageOptions[ageIndex]}
onAgeIncrement={() =>
setAgeIndex(prev => Math.min(prev + 1, ageOptions.length - 1))
}
onAgeDecrement={() => setAgeIndex(prev => Math.max(prev - 1, 0))}
documentExpiresIn={expiryOptions[expiryIndex]}
onDocumentExpiresIncrement={() =>
setExpiryIndex(prev => Math.min(prev + 1, expiryOptions.length - 1))
}
onDocumentExpiresDecrement={() =>
setExpiryIndex(prev => Math.max(prev - 1, 0))
}
ofacCheck={ofacCheck}
onOfacCheckChange={value => {
haptic.trigger('selection');
setOfacCheck(value);
}}
onResetAllValues={onResetAllValues}
onGenerateMockDocument={onGenerateMockDocument}
/>
);
};
```
- [ ] **Step 2: Verify type-check passes**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: No errors related to DevModeScreen
- [ ] **Step 3: Commit**
```bash
git add packages/webview-app/src/screens/account/DevModeScreen.tsx
git commit -m "feat(webview-app): add DevModeScreen wrapper for Euclid 3.0"
```
---
## Task 4: Wire routes in App.tsx
Add the three new routes under `/settings/*`.
**Files:**
- Modify: `packages/webview-app/src/App.tsx`
- [ ] **Step 1: Add imports and routes**
Add these imports after the existing `SettingsScreen` import (line 16):
```tsx
import { SecurityScreen } from './screens/account/SecurityScreen';
import { NotificationPreferencesScreen } from './screens/account/NotificationPreferencesScreen';
import { DevModeScreen } from './screens/account/DevModeScreen';
```
Add these routes after the `/settings` route (after line 34):
```tsx
<Route path="/settings/security" element={<SecurityScreen />} />
<Route path="/settings/notifications" element={<NotificationPreferencesScreen />} />
<Route path="/settings/dev-mode" element={<DevModeScreen />} />
```
- [ ] **Step 2: Verify type-check passes**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: PASS
- [ ] **Step 3: Commit**
```bash
git add packages/webview-app/src/App.tsx
git commit -m "feat(webview-app): add settings sub-screen routes"
```
---
## Task 5: Wire SettingsScreen menu items to real routes
Replace the five `/coming-soon` navigations with real routes and bridge actions.
**Files:**
- Modify: `packages/webview-app/src/screens/account/SettingsScreen.tsx`
- [ ] **Step 1: Update SettingsScreen menu items**
Replace the full component with updated navigation wiring. Key changes:
- "View document info" → `navigate('/coming-soon')` (no Euclid screen for this yet — keep as-is)
- "Recovery phrase" → `navigate('/settings/security')` (Security screen handles this)
- "Cloud backup" → `navigate('/settings/security')` (Security screen handles this)
- "Get support" → call `lifecycle.dismiss()` with support intent (or keep `/coming-soon`)
- "Share Self" → call `lifecycle.dismiss()` with share intent (or keep `/coming-soon`)
Additionally, add the Settings sub-sections that match the Euclid Storybook stories:
- **App settings section**: Manage Documents, Security, Notifications
- **Support section**: Support, Send feedback
- **Developer tools section** (conditional): Dev mode
```tsx
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
SettingsViewScreen,
LeftArrowIcon,
QuestionCircleStrokeIcon,
DocumentDetailsIcon,
LockIcon,
NotificationIcon,
ChatStrokeIcon,
ShareIcon,
CodeIcon,
} from '@selfxyz/euclid-web';
import { useSelfClient } from '../../providers/SelfClientProvider';
export const SettingsScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic, lifecycle } = useSelfClient();
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/');
}, [navigate, haptic]);
const onDismiss = useCallback(async () => {
haptic.trigger('selection');
analytics.trackEvent('settings_dismiss_pressed');
lifecycle.dismiss();
}, [haptic, analytics, lifecycle]);
return (
<SettingsViewScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
infoIcon={({ size, color }) => (
<QuestionCircleStrokeIcon size={size} color={color} />
)}
onClose={onBack}
showBackupInfoBox={false}
isBackupEnabled={false}
CTAs={[]}
sections={[
{
title: 'App settings',
items: [
{
icon: DocumentDetailsIcon,
label: 'Manage Documents',
description: 'Recovery phrase, passport data',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_manage_documents_pressed');
navigate('/coming-soon');
},
},
{
icon: LockIcon,
label: 'Security',
description: 'Recovery phrase, passport data',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_security_pressed');
navigate('/settings/security');
},
},
{
icon: NotificationIcon,
label: 'Notifications',
description: 'Preferences, notification types',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_notifications_pressed');
navigate('/settings/notifications');
},
},
],
},
{
title: 'Support & feedback',
items: [
{
icon: ChatStrokeIcon,
label: 'Get support',
description: 'Help center & support',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_support_pressed');
navigate('/coming-soon');
},
},
{
icon: ShareIcon,
label: 'Share Self',
description: 'Share Self with friends',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_share_pressed');
navigate('/coming-soon');
},
},
],
},
{
title: 'Developer tools',
items: [
{
icon: CodeIcon,
label: 'Dev mode',
description: 'Manage mock IDs, simulate proofs',
onPress: () => {
haptic.trigger('selection');
analytics.trackEvent('settings_dev_mode_pressed');
navigate('/settings/dev-mode');
},
},
],
},
]}
connectHeading=""
connectSubheading=""
connectButtons={[]}
bottomSectionItems={[
{
label: 'Close Self',
onPress: onDismiss,
},
]}
/>
);
};
```
- [ ] **Step 2: Verify type-check passes**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: PASS
- [ ] **Step 3: Verify Vite build succeeds**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app build`
Expected: Build succeeds, bundle output in `dist/`
- [ ] **Step 4: Commit**
```bash
git add packages/webview-app/src/screens/account/SettingsScreen.tsx
git commit -m "feat(webview-app): wire settings menu to Security, Notifications, and DevMode screens"
```
---
## Task 6: Final validation
- [ ] **Step 1: Full type-check**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app exec tsc --noEmit`
Expected: PASS with zero errors
- [ ] **Step 2: Vite production build**
Run: `cd /Users/evinova-self/Documents/self && yarn workspace @selfxyz/webview-app build`
Expected: Build succeeds. Bundle size should be similar to before (~294 KB, may increase slightly with 3 new screens)
- [ ] **Step 3: Lint check**
Run: `cd /Users/evinova-self/Documents/self && yarn lint`
Expected: No new lint errors in changed files
---
## Navigation Flow After Implementation
```
/settings (SettingsViewScreen)
├─ "Manage Documents" → /coming-soon (no screen yet)
├─ "Security" → /settings/security (SecurityScreen)
│ ├─ "Backup your account" → /coming-soon (future: backup flow)
│ ├─ "Reveal recovery phrase" → /coming-soon (future: bridge to native)
│ ├─ "Restore an account" → /coming-soon (future: restore flow)
│ └─ "Disable backups" → shows dialogue → local state toggle
├─ "Notifications" → /settings/notifications (NotificationPreferencesScreen)
│ └─ Toggle changes → local state (future: bridge to native prefs)
├─ "Get support" → /coming-soon (future: external link)
├─ "Share Self" → /coming-soon (future: share sheet via bridge)
├─ "Dev mode" → /settings/dev-mode (DevModeScreen)
│ ├─ Steppers/toggles → local state
│ ├─ "Generate mock document" → analytics event + navigate home
│ └─ "Reset all values" → reset local state
└─ "Close Self" → lifecycle.dismiss()
```
## Out of Scope
- Persisting toggle/backup state via bridge storage (marked with `// TODO` comments)
- Recovery phrase reveal flow (requires biometric auth via bridge)
- Cloud backup flow (requires native iCloud/Google backup APIs)
- Restore account flow (requires native wallet restore)
- Support link / Share sheet (requires native intents via bridge)
- Manage Documents screen (no Euclid screen exists yet)
- `@selfxyz/euclid-web``@selfxyz/euclid` package rename (separate dependency update task)

View File

@@ -142,14 +142,14 @@ yarn types # Verify type checking
yarn build # Confirm build still works
```
## SDK Architecture Specs
## SDK Architecture
For architecture context, implementation details, and workstream coordination:
For architecture context:
- **[SDK Overview](../../specs/projects/sdk/OVERVIEW.md)** — System architecture, bridge protocol, decision matrix
- **[SDK Core Spec](../../specs/projects/sdk/workstreams/sdk-core/SPEC.md)** — Implementation chunks for this package (mobile-sdk-alpha)
- **[SDK Overview](../../specs/projects/sdk/OVERVIEW.md)** — System architecture, bridge protocol, decision matrix (read-only reference)
- **Implementation specs** — Live in Linear as documents attached to issues. Check the relevant Linear issue for the current spec. Do not create spec files in `specs/` — that folder is deprecated.
Before implementing SDK work, read `CLAUDE.md` Key Rules and `specs/projects/sdk/workstreams/sdk-core/SPEC.md` for constraints and validation commands.
Before implementing SDK work, read `CLAUDE.md` Key Rules for constraints and validation commands.
## Notes

View File

@@ -10,8 +10,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@selfxyz/euclid-core": "^1.0.1",
"@selfxyz/euclid-web": "^1.0.2",
"@selfxyz/euclid": "^1.2.0",
"@selfxyz/euclid-core": "^1.2.0",
"@selfxyz/mobile-sdk-alpha": "workspace:^",
"@selfxyz/webview-bridge": "workspace:^",
"@sumsub/websdk": "^2.0.0",

View File

@@ -15,7 +15,17 @@ import { HomeScreen } from './screens/home/HomeScreen';
import { ProvingScreen } from './screens/proving/ProvingScreen';
import { VerificationResultScreen } from './screens/proving/VerificationResultScreen';
import { SettingsScreen } from './screens/account/SettingsScreen';
import { SecurityScreen } from './screens/account/SecurityScreen';
import { NotificationPreferencesScreen } from './screens/account/NotificationPreferencesScreen';
import { DevModeScreen } from './screens/account/DevModeScreen';
import { ComingSoonScreen } from './screens/ComingSoonScreen';
import { TourScreen } from './screens/tunnel/TourScreen';
import { KycMockScreen } from './screens/tunnel/KycMockScreen';
import { TunnelCountryPickerScreen } from './screens/tunnel/TunnelCountryPickerScreen';
import { TunnelIDTypeScreen } from './screens/tunnel/TunnelIDTypeScreen';
import { TunnelProofReceiptScreen } from './screens/tunnel/TunnelProofReceiptScreen';
import { TunnelProvingScreen } from './screens/tunnel/TunnelProvingScreen';
import { TunnelResultScreen } from './screens/tunnel/TunnelResultScreen';
export const App: React.FC = () => (
<BrowserRouter>
@@ -40,11 +50,21 @@ export const App: React.FC = () => (
<Route path="/proving" element={<ProvingScreen />} />
<Route path="/proving/result" element={<VerificationResultScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/settings/security" element={<SecurityScreen />} />
<Route path="/settings/notifications" element={<NotificationPreferencesScreen />} />
<Route path="/settings/dev-mode" element={<DevModeScreen />} />
<Route
path="/account/verified"
element={<VerificationResultScreen />}
/>
<Route path="/coming-soon" element={<ComingSoonScreen />} />
<Route path="/tunnel/tour/:step" element={<TourScreen />} />
<Route path="/tunnel/kyc" element={<KycMockScreen />} />
<Route path="/tunnel/registration/country" element={<TunnelCountryPickerScreen />} />
<Route path="/tunnel/registration/id-type" element={<TunnelIDTypeScreen />} />
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
<Route path="/tunnel/proof/generating" element={<TunnelProvingScreen />} />
<Route path="/tunnel/proof/result" element={<TunnelResultScreen />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</SelfClientProvider>

View File

@@ -4,7 +4,7 @@
import React, { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ComingSoonScreen as EuclidComingSoonScreen } from '@selfxyz/euclid-web';
import { ComingSoonScreen as EuclidComingSoonScreen } from '@selfxyz/euclid';
import { useSelfClient } from '../providers/SelfClientProvider';
import { getCountryName, renderFlag } from '../utils/countryFlags';
@@ -50,7 +50,7 @@ export const ComingSoonScreen: React.FC = () => {
: "We're working to roll out support for this feature."
}
description="If you'd like to be notified when this becomes available, let us know."
onSignUpPress={onNotifyMe}
onNotifyPress={onNotifyMe}
onBack={onDismiss}
renderFlag={renderFlag}
/>

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
DevModeScreen as EuclidDevModeScreen,
LeftArrowIcon,
} from '@selfxyz/euclid';
import type { IDCardProps } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
const ageOptions = ['18 or older', '21 or older', '25 or older', '30 or older'];
const expiryOptions = ['1 year', '2 years', '5 years', '10 years'];
export const DevModeScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [documentType, setDocumentType] = useState('passport');
const [nationality, setNationality] = useState('united states of america');
const [ageIndex, setAgeIndex] = useState(1);
const [expiryIndex, setExpiryIndex] = useState(2);
const [ofacCheck, setOfacCheck] = useState(true);
const idCard: IDCardProps = {
variant: 'dev-passport',
title: 'Developer Passport',
subtitle: 'Digital credential for developers',
};
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const onResetAllValues = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('dev_mode_reset');
setDocumentType('passport');
setNationality('united states of america');
setAgeIndex(1);
setExpiryIndex(2);
setOfacCheck(true);
}, [haptic, analytics]);
const onGenerateMockDocument = useCallback(() => {
haptic.trigger('success');
analytics.trackEvent('dev_mode_generate_mock', {
documentType,
nationality,
age: ageOptions[ageIndex],
expiresIn: expiryOptions[expiryIndex],
ofacCheck,
});
navigate('/');
}, [navigate, haptic, analytics, documentType, nationality, ageIndex, expiryIndex, ofacCheck]);
return (
<EuclidDevModeScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
onBack={onBack}
idCard={idCard}
documentType={documentType}
onDocumentTypePress={() => {
setDocumentType(prev => (prev === 'passport' ? 'id_card' : 'passport'));
}}
nationality={nationality}
onNationalityPress={() => {
setNationality(prev =>
prev === 'united states of america' ? 'germany' : 'united states of america',
);
}}
age={ageOptions[ageIndex]}
onAgeIncrement={() => setAgeIndex(prev => Math.min(prev + 1, ageOptions.length - 1))}
onAgeDecrement={() => setAgeIndex(prev => Math.max(prev - 1, 0))}
documentExpiresIn={expiryOptions[expiryIndex]}
onDocumentExpiresIncrement={() =>
setExpiryIndex(prev => Math.min(prev + 1, expiryOptions.length - 1))
}
onDocumentExpiresDecrement={() => setExpiryIndex(prev => Math.max(prev - 1, 0))}
ofacCheck={ofacCheck}
onOfacCheckChange={value => {
haptic.trigger('selection');
setOfacCheck(value);
}}
onResetAllValues={onResetAllValues}
onGenerateMockDocument={onGenerateMockDocument}
/>
);
};

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
NotificationPreferencesScreen as EuclidNotificationPreferencesScreen,
LeftArrowIcon,
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
const defaultToggles = [
{ key: 'self', label: 'Allow Self notifications', description: 'App updates and more' },
{ key: 'nova', label: 'Allow Nova notifications', description: 'Never miss a mission' },
{ key: 'points', label: 'Allow Self Points notifications', description: 'Points and rewards' },
{ key: 'id_status', label: 'Allow ID status notifications', description: 'Document verification updates' },
];
export const NotificationPreferencesScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [toggleValues, setToggleValues] = useState<Record<string, boolean>>({
self: true,
nova: true,
points: true,
id_status: false,
});
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const toggles = defaultToggles.map(t => ({
label: t.label,
description: t.description,
value: toggleValues[t.key] ?? false,
onToggleChange: (value: boolean) => {
haptic.trigger('selection');
analytics.trackEvent('notification_toggle_changed', { key: t.key, value });
setToggleValues(prev => ({ ...prev, [t.key]: value }));
},
}));
return (
<EuclidNotificationPreferencesScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
onBack={onBack}
toggles={toggles}
/>
);
};

View File

@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
SecurityScreen as EuclidSecurityScreen,
LeftArrowIcon,
CloudKeyIcon,
LockIcon,
ZapShieldIcon,
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
export const SecurityScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [isBackupEnabled, setIsBackupEnabled] = useState(false);
const [showDisableDialogue, setShowDisableDialogue] = useState(false);
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const onBackupAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_backup_account_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onRevealRecoveryPhrase = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_reveal_phrase_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onRestoreAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_restore_account_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onDisableBackups = useCallback(() => {
haptic.trigger('warning');
setShowDisableDialogue(true);
}, [haptic]);
const onDisableICloudBackups = useCallback(() => {
haptic.trigger('warning');
analytics.trackEvent('security_backups_disabled');
setIsBackupEnabled(false);
setShowDisableDialogue(false);
}, [haptic, analytics]);
const onDismissDialogue = useCallback(() => {
haptic.trigger('selection');
setShowDisableDialogue(false);
}, [haptic]);
return (
<EuclidSecurityScreen
insets={{ top: 0, bottom: 0 }}
escapeIcon={({ size, color }) => (
<LeftArrowIcon size={size} color={color} />
)}
cloudKeyIcon={CloudKeyIcon}
lockIcon={LockIcon}
zapShieldIcon={ZapShieldIcon}
isBackupEnabled={isBackupEnabled}
onBack={onBack}
onBackupAccount={onBackupAccount}
onRevealRecoveryPhrase={onRevealRecoveryPhrase}
onRestoreAccount={onRestoreAccount}
onDisableBackups={onDisableBackups}
showDisableDialogue={showDisableDialogue}
onDisableICloudBackups={onDisableICloudBackups}
onDismissDialogue={onDismissDialogue}
/>
);
};

View File

@@ -10,10 +10,11 @@ import {
QuestionCircleStrokeIcon,
DocumentDetailsIcon,
LockIcon,
CloudKeyIcon,
NotificationIcon,
ChatStrokeIcon,
ShareIcon,
} from '@selfxyz/euclid-web';
CodeIcon,
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
@@ -47,35 +48,35 @@ export const SettingsScreen: React.FC = () => {
CTAs={[]}
sections={[
{
title: 'Account',
title: 'App settings',
items: [
{
icon: DocumentDetailsIcon,
label: 'View document info',
description: 'View your stored document details',
label: 'Manage Documents',
description: 'Recovery phrase, passport data',
onPress: () => navigate('/coming-soon'),
},
{
icon: LockIcon,
label: 'Recovery phrase',
description: 'View your recovery phrase',
onPress: () => navigate('/coming-soon'),
label: 'Security',
description: 'Recovery phrase, passport data',
onPress: () => navigate('/settings/security'),
},
{
icon: CloudKeyIcon,
label: 'Cloud backup',
description: 'Manage your cloud backup',
onPress: () => navigate('/coming-soon'),
icon: NotificationIcon,
label: 'Notifications',
description: 'Preferences, notification types',
onPress: () => navigate('/settings/notifications'),
},
],
},
{
title: 'Support',
title: 'Support & feedback',
items: [
{
icon: ChatStrokeIcon,
label: 'Get support',
description: 'Contact us for help',
description: 'Help center & support',
onPress: () => navigate('/coming-soon'),
},
{
@@ -86,6 +87,23 @@ export const SettingsScreen: React.FC = () => {
},
],
},
{
title: 'Developer tools',
items: [
{
icon: CodeIcon,
label: 'Dev mode',
description: 'Manage mock IDs, simulate proofs',
onPress: () => navigate('/settings/dev-mode'),
},
{
icon: CodeIcon,
label: 'Tunnel flow',
description: 'Demo: register + disclose in one flow',
onPress: () => navigate('/tunnel/tour/1'),
},
],
},
]}
connectHeading=""
connectSubheading=""

View File

@@ -7,8 +7,8 @@ import { useNavigate } from 'react-router-dom';
import {
HomeScreen as EuclidHomeScreen,
GearIcon,
} from '@selfxyz/euclid-web';
import type { IDCardVariant } from '@selfxyz/euclid-web';
} from '@selfxyz/euclid';
import type { IDCardVariant } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';

View File

@@ -4,7 +4,7 @@
import React, { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { StatusState, CheckCircleIcon, colors } from '@selfxyz/euclid-web';
import { StatusState, CheckCircleIcon, colors } from '@selfxyz/euclid';
import type { VerificationResult } from '@selfxyz/webview-bridge';
import { useSelfClient } from '../../providers/SelfClientProvider';

View File

@@ -4,7 +4,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CountryPickerScreen as EuclidCountryPickerScreen } from '@selfxyz/euclid-web';
import { CountryPickerScreen as EuclidCountryPickerScreen } from '@selfxyz/euclid';
import countryDocumentTypes from '../../data/country-document-types.json';
import { useSelfClient } from '../../providers/SelfClientProvider';

View File

@@ -4,8 +4,8 @@
import React, { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { IDTypeScreen } from '@selfxyz/euclid-web';
import type { IDType } from '@selfxyz/euclid-web';
import { IDTypeScreen } from '@selfxyz/euclid';
import type { IDType } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { getCountryName, renderFlag } from '../../utils/countryFlags';

View File

@@ -4,7 +4,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Button, Title, Description, colors, spacing } from '@selfxyz/euclid-web';
import { Button, Title, Description, colors, spacing } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { useVerificationRequest } from '../../providers/VerificationRequestProvider';

View File

@@ -9,7 +9,7 @@ import {
CheckCircleIcon,
WarningOctagonIcon,
colors,
} from '@selfxyz/euclid-web';
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import type { KycProviderResult } from '../../types/kycProvider';

View File

@@ -4,7 +4,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ProofRequestScreen, SelfLogo } from '@selfxyz/euclid-web';
import { ProofRequestScreen, SelfLogo } from '@selfxyz/euclid';
import type { VerificationResult } from '@selfxyz/webview-bridge';
import { useSelfClient } from '../../providers/SelfClientProvider';
@@ -98,6 +98,8 @@ export const ProvingScreen: React.FC = () => {
appEndpoint={appEndpoint}
timestamp={timestamp}
items={proofItems}
// TODO: hardcoding for now, fetch real value
documentType='passport'
/>
);
};

View File

@@ -9,7 +9,7 @@ import {
CheckCircleIcon,
WarningOctagonIcon,
colors,
} from '@selfxyz/euclid-web';
} from '@selfxyz/euclid';
import type { VerificationResult } from '@selfxyz/webview-bridge';
import { useSelfClient } from '../../providers/SelfClientProvider';

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@selfxyz/euclid';
export const KycMockScreen: React.FC = () => {
const navigate = useNavigate();
const onContinue = useCallback(() => {
navigate('/tunnel/registration/country');
}, [navigate]);
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
gap: 32,
padding: 24,
}}
>
<h1 style={{ margin: 0, fontSize: 28 }}>KYC mock</h1>
<Button variant="primary-no-icon" text="Continue" onPress={onContinue} fullWidth />
</div>
);
};

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useNavigate, useParams, Navigate } from 'react-router-dom';
import {
LaunchTour1Screen,
LaunchTour2Screen,
LaunchTour3Screen,
LaunchTour4Screen,
} from '@selfxyz/euclid';
const insets = { top: 0, bottom: 0 };
export const TourScreen: React.FC = () => {
const navigate = useNavigate();
const { step } = useParams<{ step: string }>();
const stepNum = parseInt(step ?? '1', 10);
const onNext = useCallback(() => {
navigate(stepNum < 4 ? `/tunnel/tour/${stepNum + 1}` : '/tunnel/kyc');
}, [navigate, stepNum]);
switch (step) {
case '1':
return <LaunchTour1Screen insets={insets} onNext={onNext} />;
case '2':
return <LaunchTour2Screen insets={insets} onNext={onNext} />;
case '3':
return <LaunchTour3Screen insets={insets} onNext={onNext} />;
case '4':
return <LaunchTour4Screen insets={insets} onNext={onNext} />;
default:
return <Navigate to="/tunnel/tour/1" replace />;
}
};

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CountryPickerScreen as EuclidCountryPickerScreen } from '@selfxyz/euclid';
import { getCountryName, renderFlag } from '../../utils/countryFlags';
const MOCK_COUNTRIES = [
{ countryCode: 'US' },
{ countryCode: 'GB' },
{ countryCode: 'DE' },
{ countryCode: 'PL' },
{ countryCode: 'FR' },
];
const MOCK_DOCUMENT_TYPES: Record<string, string[]> = {
US: ['p', 'i'],
GB: ['p'],
DE: ['p', 'i'],
PL: ['p', 'i'],
FR: ['p', 'i'],
};
export const TunnelCountryPickerScreen: React.FC = () => {
const navigate = useNavigate();
const [search, setSearch] = useState('');
const onCountrySelect = useCallback(
(countryCode: string) => {
navigate('/tunnel/registration/id-type', {
state: { countryCode, documentTypes: MOCK_DOCUMENT_TYPES[countryCode] ?? ['p'] },
});
},
[navigate],
);
return (
<EuclidCountryPickerScreen
insets={{ top: 0, bottom: 0 }}
countries={MOCK_COUNTRIES}
isLoading={false}
onCountrySelect={onCountrySelect}
onClose={() => navigate('/tunnel/kyc')}
renderFlag={renderFlag}
getCountryName={getCountryName}
searchValue={search}
onSearchChange={setSearch}
/>
);
};

View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { IDTypeScreen } from '@selfxyz/euclid';
import type { IDType } from '@selfxyz/euclid';
import { getCountryName, renderFlag } from '../../utils/countryFlags';
const docTypeToIDType = (docType: string): IDType => {
switch (docType) {
case 'p':
return { id: 'p', title: 'Passport', subtitle: 'Verified Biometric Passport' };
case 'i':
return { id: 'i', title: 'ID Card', subtitle: 'Verified Biometric ID card' };
default:
return { id: docType, title: 'Unknown Document', subtitle: '' };
}
};
const renderIDTypeIcon = (idType: IDType): React.ReactNode => {
const emoji = idType.id === 'p' ? '🛂' : '🪪';
return <span style={{ fontSize: 24 }}>{emoji}</span>;
};
export const TunnelIDTypeScreen: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { countryCode = 'US', documentTypes = ['p'] } =
(location.state as { countryCode?: string; documentTypes?: string[] }) || {};
const idTypes = documentTypes.map(docTypeToIDType);
const onIDTypeSelect = useCallback(
(_idType: IDType) => {
navigate('/tunnel/proof/receipt');
},
[navigate],
);
return (
<IDTypeScreen
insets={{ top: 0, bottom: 0 }}
countryCode={countryCode}
countryName={getCountryName(countryCode)}
idTypes={idTypes}
onIDTypeSelect={onIDTypeSelect}
onBack={() => navigate(-1)}
renderFlag={renderFlag}
renderIDTypeIcon={renderIDTypeIcon}
/>
);
};

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { ProofRequestScreen, SelfLogo } from '@selfxyz/euclid';
const MOCK_ITEMS = [
{ label: 'Full Name' },
{ label: 'Date of Birth' },
{ label: 'Nationality' },
{ label: 'Age above 18' },
];
export const TunnelProofReceiptScreen: React.FC = () => {
const navigate = useNavigate();
const onConfirm = useCallback(() => {
navigate('/tunnel/proof/generating');
}, [navigate]);
const onClose = useCallback(() => {
navigate(-1);
}, [navigate]);
return (
<ProofRequestScreen
insets={{ top: 0, bottom: 0 }}
variant="default"
onClose={onClose}
onConfirm={onConfirm}
appIcon={<SelfLogo size={40} />}
appName="KYC"
appEndpoint="example.com"
documentType="passport"
timestamp={Date.now()}
items={MOCK_ITEMS}
/>
);
};

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ProofGenerationScreen } from '@selfxyz/euclid';
const MOCK_ID_CARD = {
variant: 'passport' as const,
title: 'Passport',
subtitle: 'Mock Passport',
};
export const TunnelProvingScreen: React.FC = () => {
const navigate = useNavigate();
useEffect(() => {
const timer = setTimeout(() => {
navigate('/tunnel/proof/result');
}, 3000);
return () => clearTimeout(timer);
}, [navigate]);
return (
<ProofGenerationScreen
insets={{ top: 0, bottom: 0 }}
step="generatingProof"
idCardProps={MOCK_ID_CARD}
/>
);
};

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { StatusState, CheckCircleIcon, colors } from '@selfxyz/euclid';
export const TunnelResultScreen: React.FC = () => {
const navigate = useNavigate();
const onContinue = useCallback(() => {
navigate('/');
}, [navigate]);
return (
<StatusState
variant="success"
title="Identity Verified"
description="Your identity has been verified. You can now use Self ID to prove your identity to participating partners."
buttonText="Continue"
onButtonPress={onContinue}
icon={<CheckCircleIcon size={64} color={colors.green500} />}
/>
);
};

View File

@@ -12,12 +12,12 @@ Status: Active (WebView-first + native keychain/crypto)
## Active Workstreams
| Workstream | Spec | Focus |
| ---------- | ---- | ----- |
| WebView UI | [WebView Spec](./workstreams/webview/SPEC.md) | Sumsub Web SDK integration, KYC result flow |
| SDK Core | [SDK Core Spec](./workstreams/sdk-core/SPEC.md) | Browser-portable engine |
| Workstream | Spec | Focus |
| -------------------- | ------------------------------------------------------------------- | --------------------------------------------- |
| WebView UI | [WebView Spec](./workstreams/webview/SPEC.md) | Sumsub Web SDK integration, KYC result flow |
| SDK Core | [SDK Core Spec](./workstreams/sdk-core/SPEC.md) | Browser-portable engine |
| Native Shells (Lite) | [Native Shells Lite Spec](./workstreams/native-shells-lite/SPEC.md) | Plain Kotlin + Swift for keychain/crypto only |
| Build Pipeline | [Build Pipeline Spec](./workstreams/build-pipeline/SPEC.md) | Bundle webview-app into native shells |
| Build Pipeline | [Build Pipeline Spec](./workstreams/build-pipeline/SPEC.md) | Bundle webview-app into native shells |
## Paused Workstreams

View File

@@ -110,19 +110,19 @@ On **March 20, 2026**, the active SDK delivery target was refined:
## Module Table
| Module | Location | Status | Current Role | Action Needed |
| -------------------- | ----------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| WebView UI | `packages/webview-app/` | Active | Primary product surface, KYC provider handoff, verification UX | Integrate KYC provider Web SDK (WV-05), wire result flow (WV-06) |
| SDK Core | `packages/mobile-sdk-alpha/` | Active | Shared engine for WebView/browser delivery | Keep browser entry clean and request-driven |
| WebView Bridge | `packages/webview-bridge/` | Active | Bridge protocol v1 between WebView and native shells | Stable — no changes needed |
| Android Shell | `packages/native-shell-android/` | New | Thin Kotlin shell: keychain/crypto + WebView host | Build (NSL-01) |
| iOS Shell | `packages/native-shell-ios/` | New | Thin Swift shell: keychain/crypto + WebView host | Build (NSL-02) |
| Test App | `packages/sdk-test-app/` | New | Minimal native apps for end-to-end testing | Adapt from kmp-sdk-test-app (NSL-03) |
| KMP Native Shell | `packages/kmp-sdk/` | Deprecated | Reference for native shell porting, replaced by native-shell-android/ios | Do not advance; use as port reference only |
| Swift Providers | `packages/self-sdk-swift/` | Deprecated | Reference for iOS keychain/crypto porting, replaced by native-shell-ios | Do not advance; use as port reference only |
| RN SDK | `packages/rn-sdk/` | Paused | Retained React Native shell work | Do not advance unless scope reopens |
| Native Consolidation | `app/ios/`, `packages/mobile-sdk-alpha/ios/`, related native code | Paused | Historical native cleanup and parity track | Keep as reference only for now |
| MiniPay Sample | `packages/kmp-minipay-sample/` | Paused | Historical KMP integration example | Resume only if KMP path returns |
| Module | Location | Status | Current Role | Action Needed |
| -------------------- | ----------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| WebView UI | `packages/webview-app/` | Active | Primary product surface, KYC provider handoff, verification UX | Integrate KYC provider Web SDK (WV-05), wire result flow (WV-06) |
| SDK Core | `packages/mobile-sdk-alpha/` | Active | Shared engine for WebView/browser delivery | Keep browser entry clean and request-driven |
| WebView Bridge | `packages/webview-bridge/` | Active | Bridge protocol v1 between WebView and native shells | Stable — no changes needed |
| Android Shell | `packages/native-shell-android/` | New | Thin Kotlin shell: keychain/crypto + WebView host | Build (NSL-01) |
| iOS Shell | `packages/native-shell-ios/` | New | Thin Swift shell: keychain/crypto + WebView host | Build (NSL-02) |
| Test App | `packages/sdk-test-app/` | New | Minimal native apps for end-to-end testing | Adapt from kmp-sdk-test-app (NSL-03) |
| KMP Native Shell | `packages/kmp-sdk/` | Deprecated | Reference for native shell porting, replaced by native-shell-android/ios | Do not advance; use as port reference only |
| Swift Providers | `packages/self-sdk-swift/` | Deprecated | Reference for iOS keychain/crypto porting, replaced by native-shell-ios | Do not advance; use as port reference only |
| RN SDK | `packages/rn-sdk/` | Paused | Retained React Native shell work | Do not advance unless scope reopens |
| Native Consolidation | `app/ios/`, `packages/mobile-sdk-alpha/ios/`, related native code | Paused | Historical native cleanup and parity track | Keep as reference only for now |
| MiniPay Sample | `packages/kmp-minipay-sample/` | Paused | Historical KMP integration example | Resume only if KMP path returns |
## Scope Rules

View File

@@ -32,18 +32,18 @@
## Dependencies
| Depends On | Type | Status | Notes |
|------------|------|--------|-------|
| `packages/webview-app/` | Upstream | Active | Source of the WebView bundle |
| `packages/native-shell-android/` | Downstream | Ready | Receives assets at `src/main/assets/self-wallet/` |
| `packages/native-shell-ios/` | Downstream | Ready | Receives assets at `Resources/self-sdk-web/` |
| Depends On | Type | Status | Notes |
| -------------------------------- | ---------- | ------ | ------------------------------------------------- |
| `packages/webview-app/` | Upstream | Active | Source of the WebView bundle |
| `packages/native-shell-android/` | Downstream | Ready | Receives assets at `src/main/assets/self-wallet/` |
| `packages/native-shell-ios/` | Downstream | Ready | Receives assets at `Resources/self-sdk-web/` |
## Backlog
| ID | Title | Status | Priority | Depends On | Plan | PR |
|----|-------|--------|----------|------------|------|----|
| BP-01 | WebView bundle build + copy script | Done | Medium | NSL-01, NSL-02 | [plans/BP-01-build-script.md](./plans/BP-01-build-script.md) | Complete on `feat/webview-sdk` |
| BP-02 | Runtime bundle integrity for CDN loading | Deferred | High | — | — | — |
| ID | Title | Status | Priority | Depends On | Plan | PR |
| ----- | ---------------------------------------- | -------- | -------- | -------------- | ------------------------------------------------------------ | ------------------------------ |
| BP-01 | WebView bundle build + copy script | Done | Medium | NSL-01, NSL-02 | [plans/BP-01-build-script.md](./plans/BP-01-build-script.md) | Complete on `feat/webview-sdk` |
| BP-02 | Runtime bundle integrity for CDN loading | Deferred | High | — | — | — |
Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
@@ -61,9 +61,9 @@ Trigger: when remote/CDN bundle loading is implemented.
## Active Plans
| Plan | IDs | Status |
|------|-----|--------|
| [plans/BP-01-build-script.md](./plans/BP-01-build-script.md) | BP-01 | Done |
| Plan | IDs | Status |
| ------------------------------------------------------------ | ----- | ------ |
| [plans/BP-01-build-script.md](./plans/BP-01-build-script.md) | BP-01 | Done |
## Completion Checklist
@@ -73,8 +73,8 @@ Trigger: when remote/CDN bundle loading is implemented.
## Related Specs
| Spec | Relationship |
|------|-------------|
| [SDK Overview](../../OVERVIEW.md) | Parent architecture |
| [WebView Spec](../webview/SPEC.md) | Upstream — produces the bundle |
| Spec | Relationship |
| --------------------------------------------------- | -------------------------------- |
| [SDK Overview](../../OVERVIEW.md) | Parent architecture |
| [WebView Spec](../webview/SPEC.md) | Upstream — produces the bundle |
| [Native Shells Lite](../native-shells-lite/SPEC.md) | Downstream — consumes the bundle |

View File

@@ -37,39 +37,39 @@
## Dependencies
| Depends On | Type | Status | Notes |
|------------|------|--------|-------|
| `packages/webview-bridge/` | Upstream (bridge protocol) | Done | Defines message shapes and transport names |
| `packages/webview-app/` | Upstream (WebView bundle) | Active | Native shells load this bundle |
| Build pipeline | Downstream | Ready | Copies webview-app dist into native assets |
| Depends On | Type | Status | Notes |
| -------------------------- | -------------------------- | ------ | ------------------------------------------ |
| `packages/webview-bridge/` | Upstream (bridge protocol) | Done | Defines message shapes and transport names |
| `packages/webview-app/` | Upstream (WebView bundle) | Active | Native shells load this bundle |
| Build pipeline | Downstream | Ready | Copies webview-app dist into native assets |
## Ownership Boundaries
| Area | Owner | Notes |
|------|-------|-------|
| `packages/native-shell-android/` | Native Shells (Lite) | New package |
| `packages/native-shell-ios/` | Native Shells (Lite) | New package |
| `packages/sdk-test-app/` | Native Shells (Lite) | Adapted from kmp-sdk-test-app |
| `packages/kmp-sdk/` | Paused | Reference only, do not modify |
| `packages/self-sdk-swift/` | Paused | Reference only, do not modify |
| Area | Owner | Notes |
| -------------------------------- | -------------------- | ----------------------------- |
| `packages/native-shell-android/` | Native Shells (Lite) | New package |
| `packages/native-shell-ios/` | Native Shells (Lite) | New package |
| `packages/sdk-test-app/` | Native Shells (Lite) | Adapted from kmp-sdk-test-app |
| `packages/kmp-sdk/` | Paused | Reference only, do not modify |
| `packages/self-sdk-swift/` | Paused | Reference only, do not modify |
## Backlog
| ID | Title | Status | Priority | Depends On | Plan | PR |
|----|-------|--------|----------|------------|------|----|
| NSL-01 | Android native shell (plain Kotlin) | In Progress | High | - | [plans/NSL-01-android-shell.md](./plans/NSL-01-android-shell.md) | Code complete on `feat/webview-sdk`, needs testing |
| NSL-02 | iOS native shell (plain Swift) | In Progress | High | - | [plans/NSL-02-ios-shell.md](./plans/NSL-02-ios-shell.md) | Code complete on `feat/webview-sdk`, needs testing |
| NSL-03 | Test apps (adapt from kmp-sdk-test-app) | In Progress | Medium | NSL-01, NSL-02 | [plans/NSL-03-test-apps.md](./plans/NSL-03-test-apps.md) | Code complete on `feat/webview-sdk`, needs build verification |
| ID | Title | Status | Priority | Depends On | Plan | PR |
| ------ | --------------------------------------- | ----------- | -------- | -------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- |
| NSL-01 | Android native shell (plain Kotlin) | In Progress | High | - | [plans/NSL-01-android-shell.md](./plans/NSL-01-android-shell.md) | Code complete on `feat/webview-sdk`, needs testing |
| NSL-02 | iOS native shell (plain Swift) | In Progress | High | - | [plans/NSL-02-ios-shell.md](./plans/NSL-02-ios-shell.md) | Code complete on `feat/webview-sdk`, needs testing |
| NSL-03 | Test apps (adapt from kmp-sdk-test-app) | In Progress | Medium | NSL-01, NSL-02 | [plans/NSL-03-test-apps.md](./plans/NSL-03-test-apps.md) | Code complete on `feat/webview-sdk`, needs build verification |
Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
## Active Plans
| Plan | IDs | Status |
|------|-----|--------|
| [plans/NSL-01-android-shell.md](./plans/NSL-01-android-shell.md) | NSL-01 | In Progress (code complete, needs testing) |
| [plans/NSL-02-ios-shell.md](./plans/NSL-02-ios-shell.md) | NSL-02 | In Progress (code complete, needs testing) |
| [plans/NSL-03-test-apps.md](./plans/NSL-03-test-apps.md) | NSL-03 | In Progress (code complete, needs build verification) |
| Plan | IDs | Status |
| ---------------------------------------------------------------- | ------ | ----------------------------------------------------- |
| [plans/NSL-01-android-shell.md](./plans/NSL-01-android-shell.md) | NSL-01 | In Progress (code complete, needs testing) |
| [plans/NSL-02-ios-shell.md](./plans/NSL-02-ios-shell.md) | NSL-02 | In Progress (code complete, needs testing) |
| [plans/NSL-03-test-apps.md](./plans/NSL-03-test-apps.md) | NSL-03 | In Progress (code complete, needs build verification) |
## Completion Checklist
@@ -82,19 +82,19 @@ Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
These existing files define the contract and patterns to port:
| What | Source | Notes |
|------|--------|-------|
| Bridge protocol (message shapes) | `packages/webview-bridge/src/types.ts` | Canonical — native must match |
| Bridge transport detection | `packages/webview-bridge/src/bridge.ts` | Android: `SelfNativeAndroid`, iOS: `SelfNativeIOS` |
| Crypto adapter expectations | `packages/webview-bridge/src/adapters/crypto.ts` | Defines request params and response shapes |
| Storage adapter expectations | `packages/webview-bridge/src/adapters/storage.ts` | Defines request params and response shapes |
| KMP MessageRouter | `packages/kmp-sdk/.../commonMain/.../MessageRouter.kt` | Port routing logic |
| Android SecureStorage | `packages/kmp-sdk/.../androidMain/.../SecureStorageBridgeHandler.kt` | Port EncryptedSharedPreferences pattern |
| Android WebView host | `packages/kmp-sdk/.../androidMain/.../AndroidWebViewHost.kt` | Port WebViewAssetLoader + JS interface |
| Android Activity | `packages/kmp-sdk/.../androidMain/.../SelfVerificationActivity.kt` | Port entry point, simplify permissions |
| iOS CryptoProvider | `packages/self-sdk-swift/.../CryptoProviderImpl.swift` | Port EC P-256 signing |
| iOS SecureStorage | `packages/self-sdk-swift/.../SecureStorageProviderImpl.swift` | Port Keychain Services |
| iOS WebView host | `packages/self-sdk-swift/.../WebViewProviderImpl.swift` | Port WKWebView + script message handler |
| What | Source | Notes |
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| Bridge protocol (message shapes) | `packages/webview-bridge/src/types.ts` | Canonical — native must match |
| Bridge transport detection | `packages/webview-bridge/src/bridge.ts` | Android: `SelfNativeAndroid`, iOS: `SelfNativeIOS` |
| Crypto adapter expectations | `packages/webview-bridge/src/adapters/crypto.ts` | Defines request params and response shapes |
| Storage adapter expectations | `packages/webview-bridge/src/adapters/storage.ts` | Defines request params and response shapes |
| KMP MessageRouter | `packages/kmp-sdk/.../commonMain/.../MessageRouter.kt` | Port routing logic |
| Android SecureStorage | `packages/kmp-sdk/.../androidMain/.../SecureStorageBridgeHandler.kt` | Port EncryptedSharedPreferences pattern |
| Android WebView host | `packages/kmp-sdk/.../androidMain/.../AndroidWebViewHost.kt` | Port WebViewAssetLoader + JS interface |
| Android Activity | `packages/kmp-sdk/.../androidMain/.../SelfVerificationActivity.kt` | Port entry point, simplify permissions |
| iOS CryptoProvider | `packages/self-sdk-swift/.../CryptoProviderImpl.swift` | Port EC P-256 signing |
| iOS SecureStorage | `packages/self-sdk-swift/.../SecureStorageProviderImpl.swift` | Port Keychain Services |
| iOS WebView host | `packages/self-sdk-swift/.../WebViewProviderImpl.swift` | Port WKWebView + script message handler |
## Bridge Domain Contract
@@ -102,27 +102,27 @@ Only 3 domains are implemented by the native shells:
### `secureStorage`
| Method | Params | Response |
|--------|--------|----------|
| `get` | `{ key: string }` | `{ value: string \| null }` |
| `set` | `{ key: string, value: string }` | `null` |
| `remove` | `{ key: string }` | `null` |
| Method | Params | Response |
| -------- | -------------------------------- | --------------------------- |
| `get` | `{ key: string }` | `{ value: string \| null }` |
| `set` | `{ key: string, value: string }` | `null` |
| `remove` | `{ key: string }` | `null` |
### `crypto`
| Method | Params | Response |
|--------|--------|----------|
| `generateKey` | `{ keyRef: string }` | `{ keyRef: string, success: true }` |
| `getPublicKey` | `{ keyRef: string }` | `{ publicKey: string }` (base64) |
| `sign` | `{ data: string, keyRef: string }` (data is base64) | `{ signature: string }` (base64) |
| Method | Params | Response |
| -------------- | --------------------------------------------------- | ----------------------------------- |
| `generateKey` | `{ keyRef: string }` | `{ keyRef: string, success: true }` |
| `getPublicKey` | `{ keyRef: string }` | `{ publicKey: string }` (base64) |
| `sign` | `{ data: string, keyRef: string }` (data is base64) | `{ signature: string }` (base64) |
### `lifecycle`
| Method | Params | Response |
|--------|--------|----------|
| `ready` | `{}` | `null` (no-op) |
| `dismiss` | `{ reason?: string }` | `null` (finishes Activity / dismisses VC) |
| `setResult` | `{ success: bool, userId?, verificationId?, error? }` | `null` (forwards to host, then finishes) |
| Method | Params | Response |
| ----------- | ----------------------------------------------------- | ----------------------------------------- |
| `ready` | `{}` | `null` (no-op) |
| `dismiss` | `{ reason?: string }` | `null` (finishes Activity / dismisses VC) |
| `setResult` | `{ success: bool, userId?, verificationId?, error? }` | `null` (forwards to host, then finishes) |
Any other domain request returns a `DOMAIN_NOT_FOUND` error response.
@@ -144,6 +144,7 @@ When the WebView calls `lifecycle.setResult()`, the native shell forwards the re
### Android → Host
`LifecycleHandler` calls `Activity.setResult()` with:
- `RESULT_OK` when `success: true`
- `RESULT_CANCELED` when user dismisses
- `RESULT_FIRST_USER` on error
@@ -153,6 +154,7 @@ Result JSON is included as Intent extra (`xyz.self.sdk.RESULT_DATA`). Host reads
### iOS → Host
`LifecycleHandler` invokes the `SelfSdkCallback` protocol:
- `onSuccess(result:)` when `success: true`
- `onCancelled()` when user dismisses
- `onFailure(error:)` on error
@@ -165,10 +167,10 @@ The host never sees raw KYC provider output. The WebView normalizes provider res
## Related Specs
| Spec | Relationship |
|------|-------------|
| [SDK Overview](../../OVERVIEW.md) | Parent architecture |
| [WebView Spec](../webview/SPEC.md) | Sibling — owns Sumsub integration and WebView UX |
| [SDK Core Spec](../sdk-core/SPEC.md) | Sibling — owns mobile-sdk-alpha engine |
| [Build Pipeline Spec](../build-pipeline/SPEC.md) | Downstream — bundles webview-app into native shells |
| [Paused Native Shells](../../paused/native-shells/SPEC.md) | Predecessor — KMP-based, now deprecated |
| Spec | Relationship |
| ---------------------------------------------------------- | --------------------------------------------------- |
| [SDK Overview](../../OVERVIEW.md) | Parent architecture |
| [WebView Spec](../webview/SPEC.md) | Sibling — owns Sumsub integration and WebView UX |
| [SDK Core Spec](../sdk-core/SPEC.md) | Sibling — owns mobile-sdk-alpha engine |
| [Build Pipeline Spec](../build-pipeline/SPEC.md) | Downstream — bundles webview-app into native shells |
| [Paused Native Shells](../../paused/native-shells/SPEC.md) | Predecessor — KMP-based, now deprecated |

View File

@@ -44,27 +44,27 @@ On **March 11, 2026**, the active SDK scope changed to **WebView only, with no c
## Backlog
| ID | Title | Status | Priority | Depends On | Plan | Notes |
| ----- | ----------------------------------------------------------------------------------------------- | ------ | -------- | ---------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| WV-01 | Dynamic proof request items sourced from request context | Done | High | - | [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | Existing active follow-up |
| WV-02 | Define the KYC-provider contract for document capture, MRZ/liveness handoff, and result mapping | Done | High | - | [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | Provider-backed path replaces Self-owned native scan flow; active contract is now documented |
| WV-03 | Remove native NFC and native-scan assumptions from active WebView screens, copy, and docs | Done | High | WV-02 | [plans/WV-03-remove-native-scan-assumptions.md](./plans/WV-03-remove-native-scan-assumptions.md) | Active UX/docs now route to a provider placeholder instead of Self-managed scan screens |
| WV-04 | Define the host callback contract for launch, dismiss, and final result without native modules | Done | Medium | WV-02 | [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | Browser host fallback now uses `postMessage` for iframe/popup embedding while native transports keep their current behavior |
| WV-05 | Integrate KYC provider Web SDK into ProviderLaunchScreen (Sumsub as default) | In Progress | High | WV-02 | [plans/WV-05-sumsub-web-sdk.md](./plans/WV-05-sumsub-web-sdk.md) | Code complete on `feat/webview-sdk`, needs testing |
| WV-06 | Wire KYC result through verification pipeline to host lifecycle callback | Ready | High | WV-05 | [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | Sumsub result → kycResultStore → ConfirmIdentificationScreen → lifecycle.setResult() |
| ID | Title | Status | Priority | Depends On | Plan | Notes |
| ----- | ----------------------------------------------------------------------------------------------- | ----------- | -------- | ---------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| WV-01 | Dynamic proof request items sourced from request context | Done | High | - | [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | Existing active follow-up |
| WV-02 | Define the KYC-provider contract for document capture, MRZ/liveness handoff, and result mapping | Done | High | - | [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | Provider-backed path replaces Self-owned native scan flow; active contract is now documented |
| WV-03 | Remove native NFC and native-scan assumptions from active WebView screens, copy, and docs | Done | High | WV-02 | [plans/WV-03-remove-native-scan-assumptions.md](./plans/WV-03-remove-native-scan-assumptions.md) | Active UX/docs now route to a provider placeholder instead of Self-managed scan screens |
| WV-04 | Define the host callback contract for launch, dismiss, and final result without native modules | Done | Medium | WV-02 | [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | Browser host fallback now uses `postMessage` for iframe/popup embedding while native transports keep their current behavior |
| WV-05 | Integrate KYC provider Web SDK into ProviderLaunchScreen (Sumsub as default) | In Progress | High | WV-02 | [plans/WV-05-sumsub-web-sdk.md](./plans/WV-05-sumsub-web-sdk.md) | Code complete on `feat/webview-sdk`, needs testing |
| WV-06 | Wire KYC result through verification pipeline to host lifecycle callback | Ready | High | WV-05 | [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | Sumsub result → kycResultStore → ConfirmIdentificationScreen → lifecycle.setResult() |
Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
## Active Plans
| Plan | IDs | Status |
| ------------------------------------------------------------------------------------------------ | ----- | ------ |
| [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | WV-01 | Done |
| [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | WV-02 | Done |
| [plans/WV-03-remove-native-scan-assumptions.md](./plans/WV-03-remove-native-scan-assumptions.md) | WV-03 | Done |
| [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | WV-04 | Done |
| Plan | IDs | Status |
| ------------------------------------------------------------------------------------------------ | ----- | ------------------------------------------ |
| [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | WV-01 | Done |
| [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | WV-02 | Done |
| [plans/WV-03-remove-native-scan-assumptions.md](./plans/WV-03-remove-native-scan-assumptions.md) | WV-03 | Done |
| [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | WV-04 | Done |
| [plans/WV-05-sumsub-web-sdk.md](./plans/WV-05-sumsub-web-sdk.md) | WV-05 | In Progress (code complete, needs testing) |
| [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | WV-06 | Ready |
| [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | WV-06 | Ready |
## Completion Checklist

View File

@@ -7033,6 +7033,24 @@ __metadata:
languageName: node
linkType: hard
"@lottiefiles/dotlottie-react@npm:^0.18.4":
version: 0.18.7
resolution: "@lottiefiles/dotlottie-react@npm:0.18.7"
dependencies:
"@lottiefiles/dotlottie-web": "npm:0.67.0"
peerDependencies:
react: ^17 || ^18 || ^19
checksum: 10c0/9c940db91112ef9de681c6cf8e7238d852f7f2f772b9fb2eacc214b561b22d77db965ce8dd1d3823f8a0cf69d36be9459a4b0863301ba8725aed7cdc59fd2104
languageName: node
linkType: hard
"@lottiefiles/dotlottie-web@npm:0.67.0":
version: 0.67.0
resolution: "@lottiefiles/dotlottie-web@npm:0.67.0"
checksum: 10c0/462a882b3cba7c68e575c2e1c3479afaae1cd39b16a74111b886d02a86053c1738e845d7379e31d8cc75bb66dd63c01a58b4160caa287aa5fc9a4a54dd6b72e1
languageName: node
linkType: hard
"@mapbox/node-pre-gyp@npm:^1.0":
version: 1.0.11
resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
@@ -10638,22 +10656,12 @@ __metadata:
languageName: unknown
linkType: soft
"@selfxyz/euclid-core@npm:^1.0.0, @selfxyz/euclid-core@npm:^1.0.1":
version: 1.0.1
resolution: "@selfxyz/euclid-core@npm:1.0.1"
checksum: 10c0/13b704c3fe1c8be99cd39a22a8ab22925e26c97b43f41a04e59ff313068c6806ead84f3417b6945253acaa85e2134815b8008320e9fcc4dcb15e640878a3b40e
languageName: node
linkType: hard
"@selfxyz/euclid-web@npm:^1.0.2":
version: 1.0.2
resolution: "@selfxyz/euclid-web@npm:1.0.2"
dependencies:
"@selfxyz/euclid-core": "npm:^1.0.0"
"@selfxyz/euclid-core@npm:^1.2.0":
version: 1.2.0
resolution: "@selfxyz/euclid-core@npm:1.2.0"
peerDependencies:
react: ">=18.2.0"
react-dom: ">=18.2.0"
checksum: 10c0/3fb4afe120e69bba9545ad358be1b5405c65030df35fe0c53a75adc96fe2515e79570aefe57b38e328ee653e55c20a8c09094071038e06f071ef7ee5387cfad4
checksum: 10c0/198bcbd21e674a67ac801eb2b5f6a56cb08ae9fd09904deda78f07999540d3a7ccbfffa56599ab4ced2d8204732d2d7576d63eb14983a076223b546cef31e4b0
languageName: node
linkType: hard
@@ -10670,6 +10678,20 @@ __metadata:
languageName: node
linkType: hard
"@selfxyz/euclid@npm:^1.2.0":
version: 1.2.0
resolution: "@selfxyz/euclid@npm:1.2.0"
dependencies:
"@lottiefiles/dotlottie-react": "npm:^0.18.4"
"@selfxyz/euclid-core": "npm:^1.2.0"
lottie-react: "npm:^2.4.1"
peerDependencies:
react: ">=18.2.0"
react-dom: ">=18.2.0"
checksum: 10c0/73e50bcf8540c0f2d91e3b232016c90f1019587acf24a79d9fd0e1d07cfb977aae5336972769bc824eade8ea02e02b3e583906cffa11853b389a7e2b20a34e58
languageName: node
linkType: hard
"@selfxyz/kmp-sdk-test-app@workspace:packages/kmp-sdk-test-app":
version: 0.0.0-use.local
resolution: "@selfxyz/kmp-sdk-test-app@workspace:packages/kmp-sdk-test-app"
@@ -11110,8 +11132,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@selfxyz/webview-app@workspace:packages/webview-app"
dependencies:
"@selfxyz/euclid-core": "npm:^1.0.1"
"@selfxyz/euclid-web": "npm:^1.0.2"
"@selfxyz/euclid": "npm:^1.2.0"
"@selfxyz/euclid-core": "npm:^1.2.0"
"@selfxyz/mobile-sdk-alpha": "workspace:^"
"@selfxyz/webview-bridge": "workspace:^"
"@sumsub/websdk": "npm:^2.0.0"