# Identity verification UX plan Task [#20](https://git.sendico.io/tech/sxchnge/issues/20). Plan recorded before implementation. > This original demo plan is retained as design history. Production identity > uploads, staff review and authoritative limits are implemented by task #48; > see [Customer identity verification](../identity-verification.md). Demo behavior > below applies only when the backend adapters are not selected. ## User outcome and scope Understand current access, decide whether higher limits are useful, and submit two readable photos for a human review. No KYC is a valid account state. Email confirmation and identity verification remain separate. The existing app is a demo: this release simulates review, holds photos only in memory and never uploads them. It must say so before selection and submission; use sample images only. ## Journey 1. **Verification & limits**: status, active tier, basic versus verified daily and monthly limits, and one relevant next action. Preview policy is explicitly labeled as fixtures in the stub service. A draft, pending review, correction request or denial keeps basic limits; only approval enables the verified tier. This is presentation, never financial authorization. 2. **Passport**: photograph the open identity/photo page of a valid passport; all edges visible, readable text, no glare or fingers covering details. 3. **Selfie with passport**: face and open passport visible in the same photo, unobstructed and in focus. Explain the difference with an icon and guidance. Each slot supports JPEG/PNG up to 10 MiB, preview, replacement and removal. Validate content/decodability, size and dimensions, not just extensions. 4. **Review**: both previews, ability to return and replace, explicit confirmation that photos belong to the user, then submit for human review. No automatic approval, invented review SLA or instant increase in limits. 5. **Review status**: pending message, unchanged limits and manual refresh. A correctable rejection has a specific safe reason and a fresh submission path; final denial offers support guidance and no futile resubmit control. Approval clearly shows the active higher tier. No forced upgrade or dead-end gate. Back preserves the in-memory draft; leaving the flow asks whether to discard selected photos. Refresh/closing clears a demo session. Successful submission releases photo bytes. Never put images, paths or filenames in routes, analytics, logs, preferences or task records. Demo decision controls are explicitly separate from customer actions and operate only with the dummy service. ## State and recovery contract | State | Tier | Action | |---|---|---| | Not verified | Basic | Start optional verification | | Local draft | Basic | Continue / discard | | Loading status | Unknown | Wait; don't assert basic/verified before response | | Status unavailable | Last known, marked stale | Retry; block new submission until refreshed | | In review | Basic | Refresh; continue account use | | Changes requested | Basic | Reason: unreadable passport, unreadable selfie, expired passport; replace and resubmit | | Denied | Basic | Explain decision category; contact support for review | | Verified | Verified | See increased limits | Picker cancellation is neutral. Invalid type, oversized/corrupt/too-small image, permission/read failure and unavailable submission have distinct recoverable messages. Keep valid photos on failed replacement/submission. Serialize operations, prevent duplicate submits, ignore results after disposal. Server adapters must provide idempotent submissions and current status/revision, enforce account and email access, handle session expiry and rate limits, and validate images again. Do not infer identity validity from client image checks. ## Measurement Use existing typed AnalyticsService/JourneyRecorder. A KYC submission journey starts when the user starts/restarts verification. Semantic stages: preparation, passport selection, selfie selection, submission. Replacements/retries are operations in the same journey. Confirmed service acceptance is the terminal submission outcome, **not identity approval**; demo outcomes are simulated. Explicit draft discard/navigation or 30 minutes of settled inactivity marks abandonment. Do not count time waiting for human review as abandonment. Review status refresh is a separate terminal observation, not a new approval conversion. Measure start-to-submission conversion, per-stage failures and recovery, elapsed submission time and explicit/inactivity abandonment. Only enum reason categories, random journey IDs, operation sequence and elapsed time are collected. No photo content/metadata, names, emails, document numbers, URLs or exception strings. Disable third-party session replay/autocapture on identity routes when PostHog is added. Production time-to-decision and approval rate require server-side review events correlated by an opaque submission ID; frontend refresh cannot establish those metrics. Cross-session resume requires the future backend. ## Visual and accessibility plan Reuse theme colors/typography, rounded panels, locale selector and GoRouter. Single scrollable mobile column; constrained desktop reading width. Text labels and icons accompany status colors. Visible numbered steps, back/skip controls, minimum touch targets, keyboard-accessible buttons, live-region operation/error feedback, image semantic labels without filenames. Check EN/RU, 320px viewport, large text, success/failure paths and analytics isolation before delivery. ## Production integration prerequisites This frontend scope does not implement a staff backoffice, document storage or a real KYC policy. An authenticated backend must own entitlements, limits and review transitions. Provide private upload/download authorization, human-review roles, audit trail, safe rejection categories, retention/deletion policy, user privacy notice/support destination, idempotency and malware/content checks before enabling real documents. No production claims, deadlines or statutory requirements are invented here. Native camera capture can later complement the shared file picker. ## Sendico limits alignment Inspected Sendico ADR 0001 and `api/proto/limits/v1/limits.proto` (`ListVisibleLimits`, `VisibleLimitSlot`). No matching Flutter limits provider was found in that checkout. Follow its ownership boundary: LimitsService.fetch() returns backend-evaluated tier, visible caps, remaining capacity and upgrade comparison. LimitsProvider loads/refreshes those models; widgets never derive active limits from KYC. The dummy service performs the backend stand-in evaluation. The visible projection does not reserve capacity or authorize payments. ## Implementation and review evidence - Optional `/identity` demo entry from login and confirmed-email screens. No live session is implied. GoRouter exit guard and discard confirmation protect drafts; reload/closing the browser remains destructive to this explicitly transient demo. - Shared package uses transport DTO → mapper → service → model → provider. Limits are independently fetched and expose current/remaining/upgrade values. - Typed analytics separates submission from status observation; picker cancellation has a neutral cancelled signal. Simulated submission is not real identity approval. - Review fixed a Flutter Web incompatibility: encoded ImageDescriptor dimensions are unsupported. Image metadata is now read before bounded decoding using the cross-platform image package. Actual browser file selection with a generated 640×640 PNG succeeds for both slots and reaches pending review with basic limits. - EN/RU widget journeys cover approval, correction/resubmission, denial/support, errors, discard, and 320px width at 1.6 text scale. Controls use theme colors and labels; headings/errors expose live-region semantics, step changes move focus and scroll context. Native VoiceOver/TalkBack and device camera QA remain release checks for future mobile consumers; no camera capture is implemented here. - Photo previews evict their image-cache entry on replacement/disposal. Providers release photo references on exit/submission/disposal. No secure-memory-erasure guarantee or browser-termination telemetry is claimed. ## Measurement definitions for dashboards The denominator for submission conversion is journeys containing a requested `kycPreparation` step; status-only refresh journeys are excluded. Numerator is a terminal `kycSubmission` service outcome, counted once per journey. Exclude `simulated=true` from production dashboards. Stage failure rate divides failed operations by requested operations of that stage; cancelled pickers are reported separately. Compare recovered submissions within the same journey, and calculate duration from the first preparation request to acceptance. Explicit exit and settled 30-minute inactivity are separate abandonment categories. Refresh after an uncertain submission reconciles server status but cannot prove an end-to-end submission conversion; production correlation needs the backend submission ID. Final validation: format check (27 Dart files), analyzer in all three affected packages, `make test-frontend` (86 tests) and release web build passed. The UX checklist was reviewed against the implemented demo scope; production/backend and native assistive-technology gaps are recorded above rather than represented as complete. ## Limits usage correction — task #22 Visible limits include backend-provided committed (`usedMinor`) and reserved (`reservedMinor`) usage, matching Sendico's `VisibleLimitSlot`. Show both amounts, the current cap and authoritative remaining capacity for each period. A labeled theme-based indicator visualizes used plus reserved against the cap; clamp only the visual ratio, never rewrite the returned monetary values. No capacity is communicated in text when remaining is zero. Daily/monthly demo fixtures now have nonzero activity, retained when caps increase after approval. This is a display correction within the existing KYC journey, so no additional analytics events or financial data collection are introduced. Tests cover mapping without inferring remaining, unused/exhausted/over-cap/zero-cap ratios and EN/RU usage display at 320px with 1.6 text scaling. ## Residence country selection — task #64 Task [#64](https://git.sendico.io/tech/sxchnge/issues/64), branch `kyc-64`. Delivery: [PR #65](https://git.sendico.io/tech/sxchnge/pulls/65). The profile now shows a country name and opens a searchable country list instead of asking customers to type a two-letter code. Names follow the English/Russian app locale. Search accepts localized or English names and two-letter codes, ignoring case and surrounding whitespace. Selection updates only `IdentityProfile.residenceCountry` with the country code; the existing provider, service, mapper and transport contracts remain unchanged. The `country_picker` dependency supplies bundled country data, code lookup and translations through `CountryService` and `CountryLocalizations`. Flutter Material controls provide the search interface, explicit no-results recovery, minimum touch targets, keyboard selection, a selected-state checkmark, Back and Escape. The package's built-in list omits explicit no-results feedback and uses shorter row targets, so its data/localizations are reused with standard Material controls. There is no remote lookup, copied country table or extra application provider. Cancelling preserves the current profile, prefilled codes display names, busy state disables opening the selector, and the search controller is disposed with its dialog. The list is a data catalogue; backend eligibility remains authoritative. This change stays within the existing KYC preparation/submission journey and its measurement definitions above. No selector events, queries, names or country codes are added to telemetry. Existing acceptance/failure/retry/abandonment signals and documented server-side review measurement gaps still apply. Focused widget checks cover name/code search, code-only updates, reselection, cancel, no-results recovery, clearing search, saved values, locale changes, Russian names, disabled state, keyboard activation/selection/Escape and labeled touch targets. A long country name fits at 320px with 1.6 text scale. Existing EN/RU verification journeys now select a country and assert the profile code before continuing through document submission. Native screen-reader and device keyboard testing remain release checks; widget semantics checks do not establish those device outcomes. Validation: five changed Dart files passed the format check; `flutter analyze` in `frontend/app` reported no issues; `make test-frontend` passed all 222 tests; and `flutter build web` passed, including its Wasm compatibility dry run. Browser smoke testing on the local demo opened the selector, searched for Poland and confirmed that the profile displays Poland after selection. Focused and full journey tests verify the stored `PL` code. Generated localization/build outputs remain ignored and uncommitted. ## Birthday entry — task #67 Task [#67](https://git.sendico.io/tech/sxchnge/issues/67), branch `kyc-67`. Delivery: [PR #70](https://git.sendico.io/tech/sxchnge/pulls/70). This change is stacked on the residence-country selector in PR #65; merge #65 before the birthday PR. The profile uses labeled Day, Month and Year controls. Day and year accept numeric entry with birthday autofill hints; month selection uses localized month names and standard Material keyboard/touch interaction. Customers can enter their birth year directly. No calendar paging or transport-format punctuation is required. The controls share a row when space allows and stack at narrow widths or enlarged text sizes. English and Russian labels, month names and errors follow the app locale. Valid calendar dates are normalized to the existing `IdentityProfile.dateOfBirth` `YYYY-MM-DD` value through `IdentityProvider.updateProfile`. Partial, impossible and future dates clear that profile value so Continue cannot retain an earlier valid birthday. Local input components remain visible while correcting a date; complete invalid dates receive a specific live-region error below the group. Leap years, zero day/year and calendar rollover are checked. Today's date follows the existing model policy; no minimum age or arbitrary oldest birth year is added. Prefilled dates, locale changes and external profile replacement synchronize the controls. Back navigation restores the valid saved date. Busy state disables all three controls. Controllers are disposed with the widget. The existing DTO, mapper, service, domain model and provider boundaries are retained. No dependency, transport or backend change is needed. This remains part of the KYC preparation-to-submission journey defined above: existing acceptance, failure, retry and abandonment signals apply, without new field events or date collection. The existing server-side review measurement gaps remain unchanged. UX review and automated checks cover entry/correction, leap dates, invalid and future-date recovery, numeric restrictions, prefill/replacement/clearing, locale switching, disabled state, Tab/Enter/arrow-key month selection, Escape cancellation, and Russian controls/errors at 320px with 1.6 text scale. Labeled and minimum-size tap-target guidelines pass. Integration checks verify that invalid dates block Continue, Back preserves the birthday, and EN/RU flows still reach submission acceptance. Native screen-reader and mobile keyboard/autofill testing remain release checks; widget semantics are not a claim of device-level validation. Validation: all four changed Dart files pass the format check; `flutter analyze` in `frontend/app` reports no issues; `make test-frontend` passes all 230 tests, including the seven new birthday widget tests and the new navigation/gating test. `flutter build web` passes, including its Wasm compatibility dry run. Browser smoke testing of the release build entered 23 April 1990, verified the inline error for 31 April and restored 23 April without losing month or year. Generated localization and build outputs remain ignored and uncommitted.