360 lines
22 KiB
Markdown
360 lines
22 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.
|
|
|
|
## Verification links and login handoff
|
|
|
|
Task: [#60](https://git.sendico.io/tech/sxchnge/issues/60), branch
|
|
`email-verification-60`. Email links use `/verify-email?token=…`; the web entry
|
|
point installs Flutter's path URL strategy before initializing the application.
|
|
This matches Caddy's existing SPA fallback and preserves the path and query on a
|
|
fresh page load. Previously the default hash strategy could start at login and
|
|
never submit the activation token. Unknown routes retain the canonical login
|
|
fallback; invalid, expired and failed confirmation attempts remain on the
|
|
verification page with the existing recovery actions.
|
|
|
|
Activation consumes the token, checks the pending account and accepted signup
|
|
facts, and activates the account in the existing database transaction. Only
|
|
after a successful commit does the API return `{verified: true, email: ...}`.
|
|
Failure responses do not disclose the account email. Responses retain the
|
|
shared no-store policy. The frontend backend DTO and mapper produce a
|
|
`VerificationResult`; the verification provider exposes that result only in the
|
|
verified state and clears it on reset, loading and failure. A verified response
|
|
from an older backend without the optional email can still show success.
|
|
|
|
Success stays visible until the user chooses **Continue to login**. This replaces
|
|
the misleading workspace action that could redirect an unauthenticated visitor
|
|
back to login. The verified email travels through transient router extras into
|
|
the editable login controller; the password starts empty and receives focus.
|
|
Normal password and second-factor checks remain required. The email is not
|
|
added to URLs, serialized history, persistent storage or analytics. A link opened
|
|
in another tab or device can receive the verified address from the API without
|
|
depending on the originating signup form's memory.
|
|
|
|
Existing requested/succeeded/failed email-confirmation journey signals remain
|
|
the measurement source. Success continues to mean that the confirmation service
|
|
returned a verified result, with simulated outcomes kept separate. No field or
|
|
prefill event is needed. Existing limits around pre-submit abandonment,
|
|
cross-device journey correlation and the in-memory analytics sink still apply.
|
|
|
|
Regression coverage includes transactional activation and rollback, returned
|
|
email and failure privacy, used-token rejection, backend response mapping,
|
|
provider identity clearing, EN/RU success-to-login navigation, editable prefill,
|
|
empty focused password, and exclusion of email from serialized navigation state.
|
|
The supplied private diagnostic token is not a test fixture or documentation
|
|
input; it is kept out of repository sources and tracker records.
|
|
|
|
### Validation and local release (#60)
|
|
|
|
Delivery: [PR #61](https://git.sendico.io/tech/sxchnge/pulls/61), implementation
|
|
commit `5f3b443`. Reviewed the complete backend and frontend contribution against
|
|
their module guidelines and the UX/observability checklists. No task-caused
|
|
review findings remain. Existing recovery, loading and accessible status states
|
|
are retained; EN/RU widget tests and existing small-screen/text-scale coverage
|
|
pass. Browser startup was checked against a separately built demo with synthetic
|
|
tokens: direct success remains visible, Continue to login shows the verified
|
|
email with an empty focused password, and an expired link offers resend/back.
|
|
Native Safari was unavailable to automation, so browser-specific helper behavior
|
|
is not claimed as verified by this check.
|
|
|
|
On 2026-10-19, changed-file Dart formatting, `flutter analyze` in both affected
|
|
packages (`frontend/app`, `frontend/packages/sxchnge_auth`), `make test-frontend`
|
|
(217 tests), `GOFLAGS=-p=1 make test`, and `make lint-backend` passed. The focused
|
|
transactional activation integration test also passed independently. Initial
|
|
test-container startup and image-resolution attempts failed while Docker was
|
|
stalled. After the user-authorized Docker Desktop restart, the unchanged checks
|
|
passed; no assertions, timeouts or gates were weakened. No security scan was
|
|
requested or run.
|
|
|
|
The task branch was integrated locally into the existing `identity-facts-57`
|
|
stand checkout, preserving its unrelated configuration. Fresh stable image
|
|
versions/digests were resolved successfully with `sh infra/local/tools.sh images`.
|
|
Compose rebuilt BFF and frontend with `build --pull bff frontend`, then recreated
|
|
them and the Sendico session sidecar (which shares BFF's network namespace).
|
|
Docker restart recovery also unsealed Sendico's existing Vault with its saved
|
|
local key and restarted affected existing services in dependency order.
|
|
|
|
The updated BFF, frontend and Sendico sidecar are healthy. The authenticated
|
|
Sendico currency/ledger check passes. HTTPS serves the direct verification path;
|
|
the served JavaScript matches the new frontend image with SHA-256
|
|
`b194c0cf5dcc62634c13c7fd3d2f7e482272cb7abc1e470c62629400bf2d5d21`.
|
|
A live synthetic invalid token returns HTTP 400 without an email. The supplied
|
|
real token was checked read-only and remains unused and unexpired, with its
|
|
account pending verification, ready for the user to reopen the link. PR #61 has
|
|
not been merged into remote main; this local deployment does not imply a main
|
|
branch merge.
|
|
|
|
### Accepted signup delivery (#62)
|
|
|
|
Task [#62](https://git.sendico.io/tech/sxchnge/issues/62), branch
|
|
`email-verification-62`. On October 23, signup returned HTTP 202 and the local
|
|
SMTP provider accepted its message with status 250, while the user reported an
|
|
internal-error screen immediately after Create account. No subsequent
|
|
verification request from that attempt reached the BFF. The precise original
|
|
client exception was not captured; an ordinary isolated release reproduction
|
|
succeeded. The regression below deliberately injects an unavailable follow-up
|
|
send service to cover the post-acceptance failure path without claiming that
|
|
injected exception was the original cause.
|
|
|
|
Backend and preview signup mappers now return a `VerificationDelivery` domain
|
|
receipt with the recipient and simulation status. It means that signup accepted
|
|
the email-queue operation, not inbox delivery, account activation or a session.
|
|
The signup provider exposes it through its operation result. The form captures
|
|
the receipt before clearing password controllers, whose change listeners reset
|
|
auth state, and passes it as transient router extra. The verification provider
|
|
initializes directly in the sent state; mounting the page performs no send.
|
|
This replaces the backend adapter's hidden one-use signup cache. Every explicit
|
|
resend now invokes its service, including the first one after signup.
|
|
|
|
Routes without a receipt retain their existing behavior: unverified login may
|
|
request an email, a fresh verification page asks for an address, and emailed
|
|
links run confirmation. Recipient changes invalidate the displayed claim until
|
|
another send succeeds. New receipts replace stale recipients. The route codec
|
|
discards the receipt when serializing history, so reloads do not invent a queued
|
|
result or retain the address. No token or password is included in the receipt.
|
|
|
|
The existing email-delivery requested/succeeded signals now follow the accepted
|
|
signup receipt inside the instrumented service instead of a synthetic send on
|
|
page mount. They stay in the signup journey, occur once for the accepted
|
|
operation, and remain intermediate queue-acceptance signals. Resends retain
|
|
separate attempts and failures; confirmation remains the terminal outcome.
|
|
Simulation and privacy rules are unchanged, including the in-memory analytics
|
|
and cross-device correlation limitations above.
|
|
|
|
Frontend review covered the complete app/auth contribution and preserved the
|
|
DTO, mapper, service, domain model, provider and UI boundaries. Regressions cover
|
|
accepted signup followed by unavailable resend, explicit retry, normalized
|
|
receipt mapping, provider initialization/reset, recipient replacement, transient
|
|
history, event correlation and exclusion of form values from analytics. EN/RU
|
|
flow tests run at 390px with 1.5x text scaling; existing keyboard, autofill and
|
|
accessibility suites also pass. Changed-file Dart format checks, analysis in
|
|
`frontend/app` and `frontend/packages/sxchnge_auth`, and `make test-frontend`
|
|
(221 tests) pass. The new injected-failure regression failed on the previous
|
|
implementation and passes with the receipt handoff. A release-browser fixture
|
|
with the same backend adapter and a failing resend service shows Check your
|
|
inbox after signup with no send call, then the expected error only after an
|
|
explicit resend. It sends no email. Native Safari was not automated.
|
|
|
|
The frontend image was rebuilt with freshly resolved stable digest pins and
|
|
recreated without restarting BFF. The served JavaScript matches the new image
|
|
at SHA-256 `0a45ce6f0b4658b1f03d8e39b9e39fd3fbfcf6f1626cb3f17705dce2da130a6c`.
|
|
Normal browser-trust HTTPS serves the frontend and API with HTTP 200.
|
|
`sh infra/local/tools.sh verify` passes all seven services, Vault/database
|
|
checks and authenticated Sendico currency/ledger reads. No new customer email,
|
|
activation or payment was submitted for this change, and no security scan was
|
|
requested or run. The user's pre-existing browser tab must reload to use the
|
|
new bundle; their original Safari exception remains unobserved.
|