261 lines
16 KiB
Markdown
261 lines
16 KiB
Markdown
# 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`.
|
||
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.
|