Files
sxchnge/docs/ux/auth-journeys.md

217 lines
13 KiB
Markdown

# Authentication UX and analytics contract
Task: [#12](https://git.sendico.io/tech/sxchnge/issues/12). Applies to login,
signup through email verification, and standalone confirmation links.
## User outcomes and interface
Signup leads to email delivery, then confirmation. Sent status shows a fixed
recipient; Change email explicitly leaves that state and focuses the editor.
A temporary confirmation failure prioritizes Retry verification, with requesting
a replacement email secondary. Expired/invalid links offer replacement delivery.
Demo labels sit beside the heading, and demo outcomes explicitly state that no
email was delivered or account verified. The demo confirmation link is the
primary next step on the simulated sent screen. EN/RU use the installed theme.
## Measurement scope and attempts
Owner/consumer: the sxchnge product team, diagnosing onboarding completion and
choosing between fixes to registration, delivery and confirmation recovery.
A journey starts at the first **locally valid service request**. This intentionally
measures submitted attempts, not page views or field editing; pre-submit form
abandonment and validation friction are outside this denominator. No generic
click/field events are collected.
- `login`: one requested login through its service result.
- `signup`: signup acceptance, email delivery requests, then email confirmation.
- `verification`: confirmation/replacement delivery when there is no active signup.
Retries reuse the journey ID and get a new operation number. Signup success and
email delivery are intermediate states; confirmation success is the onboarding
terminal outcome. Login success is terminal. Failed operations remain recoverable.
Every operation has one completion, and every journey at most one terminal event.
Changing auth mode, returning from verification to login, changing a confirmation
token, or disposing the app ends the open attempt explicitly. A new operation
after a terminal outcome starts a new journey.
The inactivity window is **30 minutes after the last settled nonterminal service
result**, reset by a new request. It is suspended while a service operation is
pending: missing completion is not assumed to be abandonment. An explicit exit
can end an attempt while work is pending; it does not cancel backend work. Late
results cannot reopen that attempt or emit success after its abandonment.
IDs are random, scoped to this app instance and never derived from identity or
link tokens. Same-instance signup → demo link is correlated. A real email opened
in another tab/device or after reload starts a standalone verification journey.
Cross-device signup conversion needs a future backend-issued, non-sensitive
correlation contract; client code must never use email or the token as that ID.
Tab termination is not reliably observable. Missing exits remain **unknown**,
not inferred abandonment. Background timer throttling can delay idle detection.
## Event schema (version 1)
| Event | Emission condition | Product decision supported |
| --- | --- | --- |
| `auth_journey_started` | First valid request for a new attempt | Submitted-attempt denominator |
| `auth_{step}_requested` | A domain service operation starts | Locate the last meaningful step and retries |
| `auth_{step}_succeeded` | Service confirms a non-simulated result | Step conversion and outcome duration |
| `auth_{step}_simulated` | Dummy result, or terminal outcome of a mixed simulated journey | Validate the preview funnel without reporting business conversion |
| `auth_{step}_failed` | Service operation fails | Failure rates by safe error category and recovery |
| `auth_journey_abandoned` | Explicit exit or settled inactivity timeout | Diagnose known abandonment/drop-off |
`step` is `login`, `signup`, `email_delivery`, or `email_confirmation`.
Properties: schema_version, event_id, journey_id, journey_kind, sequence,
operation (per-journey counter when applicable), occurred_at (UTC), elapsed_ms
(monotonic journey duration), terminal, simulated, and optional typed failure/exit.
Every event in a known dummy journey is marked simulated. No email, password,
name, user ID, confirmation token, URL, raw exception, or form contents is captured.
## Metrics
Group by journey_kind and journey_id; deduplicate delivery by event_id, ordering
by sequence. Use **closed attempts** (terminal outcome or explicit abandonment)
for settled cohort rates; report open/unknown counts separately. For a fixed
reporting cohort, include starts in that interval and allow their defined
inactivity window to settle. Report pending operations separately even after it.
- Completion: distinct successful terminal journey IDs / closed journey IDs.
- Simulated completion: same calculation using simulated terminals, reported
separately. Never mix these into actual account/session conversion.
- Failure rate: distinct failed operations / requested operations; also report
journeys with any failure / started journeys.
- Recovery: closed journeys completing after a failed operation / closed
journeys with a failed operation.
- Known abandonment: abandoned journey IDs / closed journey IDs, by exit reason.
- Step drop-off: closed journeys reaching a step but not the next / closed
journeys reaching that step (signup acceptance → delivery → confirmation).
- Duration: terminal elapsed_ms; step latency from matching requested/result
operation timestamps. Report distributions separately for retries and failures.
Current service results are client evidence only. Future production adapters must
return success only after server confirmation; delivery API acceptance still
isn't proof that the email reached an inbox. That remains a documented proxy.
## Dummy service and PostHog replacement
`sxchnge_analytics` is a platform-independent shared package. `AnalyticsService`
accepts typed `AnalyticsEvent`; `DummyAnalyticsService` retains the latest 500
unique events in memory for tests/debugging. It exposes an immutable event list
and clear(), writes no logs/files, installs no SDK, and makes no network requests.
It is diagnostic storage, not a durable metrics backend; older events are evicted.
Inject the service through `SxchngeApp(analyticsService: service)`. Auth services
are decorated by `InstrumentedAuthServices`, so rebuilds, locale changes and
layout changes cannot generate events. Analytics exceptions are swallowed at the
recorder boundary and analytics futures are never awaited by auth operations.
Adapters must do lightweight capture/enqueue work; synchronous CPU blocking in an
adapter cannot be isolated by a Dart Future.
A future PostHog adapter should implement capture using event.name and the
allowlisted event.properties. Official API reference:
https://pub.dev/documentation/posthog_flutter/latest/posthog_flutter/Posthog-class.html
Preserve event_id and sequence for deduplication/reconstruction. Configure consent,
retention, region and anonymous identity before enabling collection. Do not enable
autocapture, session replay, automatic URL capture or user identification as a
side effect of this adapter: confirmation URLs and authentication forms contain
sensitive data. No PostHog key or network integration is included in this task.
Retries/buffering in a future adapter must preserve the original event_id and
occurred_at, remain bounded, and never delay authentication.
## UX review and verification
- Purpose/primary action: explicit demo outcomes and primary confirmation retry.
- Context/recovery: confirmed recipient separate from editor, focus restored by
Change email, no sent claim while editing; service failure retains input.
- States: initial/loading/sent/verified/expired/invalid/internal error remain
available. Restricted/offline backend behavior is outside this dummy service.
- Responsiveness: EN/RU and 200% text coverage retained; focused tests cover
keyboard submission, semantic heading/live region and mobile target labels/sizes.
- Measurement: tests cover correlated success, simulated outcomes, failures,
retries, duplicate completion/delivery, explicit exit, pending operations,
inactivity, and throwing/failing/non-completing analytics adapters.
- Privacy: payload tests exclude supplied form values and tokens; only typed
allowlisted properties are exposed. Dummy buffer is bounded and read-only.
- Remaining limits: no real PostHog delivery or cross-device correlation is
claimed. Automated semantics checks do not replace assistive-technology testing
on supported devices; a native VoiceOver/TalkBack audit remains a release check.
### Recorded validation
The automated suite passes 68 tests: 7 analytics, 20 auth, 3 theme, 3 theme
consumer, and 35 app tests. Formatting and analysis pass for the three affected
packages; the web build passes. Browser walkthrough at 390px covers internal-error
hierarchy, secondary email recovery, actual Tab/Enter submission, and the demo
sent screen. The heading is exposed as a live accessibility heading. Recipient
text uses a normal text node to remain exposed to assistive technology.
Delivered in [PR #13](https://git.sendico.io/tech/sxchnge/pulls/13), branch
`onboarding-12`. The final browser accessibility tree exposes the recipient text,
demo status, status heading and recovery actions. PR is open for review.
## Browser credential autofill
Task: [#56](https://git.sendico.io/tech/sxchnge/issues/56), branch `auth-56`.
Delivery: [PR #58](https://git.sendico.io/tech/sxchnge/pulls/58), implementation
commit `86b6aaf`. Open for review; no merge or deployment is included.
The consumer auth form groups its mounted inputs in one `AutofillGroup` and
declares their purpose explicitly:
| Input | Flutter hints | Browser autocomplete |
| --- | --- | --- |
| Signup name | `name` with the name keyboard | `name` |
| Account email | `username`, then `email`, with the email keyboard | `username` |
| Login password | `password` | `current-password` |
| Signup password and confirmation | `newPassword` on both | `new-password` |
Flutter uses the first hint on web and Apple platforms. Email is the credential
username; the email keyboard remains configured separately. Visibility toggles
keep the password role, so autofill does not depend on detecting masked input.
The confirmation field participates in the same group as the first password,
allowing the browser to fill both without copying one controller into the other.
Signup acceptance finalizes autofill before clearing password controllers and
navigating to email verification. Login finalizes on success; when a second
factor is required, it waits for verification and login completion. The password
remains only in the existing form controller during that challenge, then clears
on completed navigation or cancellation. Invalid input, rejected credentials and
failed verification do not request saving. Switching auth mode, abandoning the
form or leaving for unverified-account recovery cancels the autofill context.
Provider/service/model boundaries and existing request/outcome telemetry are
unchanged; no field events or credential telemetry are added. Browser popup
display and pre-submit autofill abandonment remain outside current measurement.
### Verification and release check
Focused widget tests send a platform autofill response with all four signup
values and check EN/RU controller updates, grouped credential metadata, visibility
changes, mode switching and abandonment. Auth tests cover validation failures,
service rejection/retry, stale responses, signup/login acceptance, unverified
recovery, invalid second-factor codes and cancellation without premature saving.
Existing narrow/desktop layouts and enlarged-text tests cover the unchanged UI.
Native Safari verification remains a release check: computer-control permission
was unavailable in this session. On the deployed HTTPS origin with Safari
AutoFill enabled, focus the signup email and password fields, choose a suggested
strong password, verify that both password fields populate, then confirm that
login offers the saved credential. Repeat after showing/hiding passwords and
switching modes. Browser settings, stored credentials and browser heuristics
determine whether suggestions appear; automated Flutter tests cannot establish
that the system popup was displayed or that credentials were saved.
References: [Apple Password AutoFill](https://developer.apple.com/documentation/security/enabling-password-autofill-on-an-html-input-element),
[Flutter autofill hints](https://api.flutter.dev/flutter/material/TextField/autofillHints.html),
and [AutofillGroup](https://api.flutter.dev/flutter/widgets/AutofillGroup-class.html).
Recorded validation: `dart format --output=none --set-exit-if-changed` passes for
all five changed Dart files; `flutter analyze` passes in `frontend/app`;
`make test-frontend` passes all 212 tests across the app and shared packages.
`flutter build web` passes. The built page's default rendering path was inspected
in the in-app browser: login exposes `username`/`current-password` in one HTML
form; signup exposes `username`, `name` and two `new-password` inputs in one form.
This verifies the browser metadata, not Safari's credential popup. With Flutter
web accessibility semantics enabled, unfocused semantic inputs initially use
framework-default autocomplete values until focused; password-manager behavior
with assistive technology still requires a native browser audit.