Fix onboarding UX and add replaceable journey analytics
Make recipient changes explicit, prioritize verification retry, and label simulated outcomes prominently. Add shared typed analytics with bounded dummy capture, correlated service events and documented PostHog adapter contract. Validate privacy, retries, abandonment, keyboard recovery and accessibility. Related to #12.
This commit is contained in:
1 parent
bf7778f312
commit
f1cbf64282
28 files changed
+1158
-64
No files matched your search
@@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
|
||||
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
|
||||
import '../auth/brand_panel.dart';
|
||||
@@ -28,6 +30,8 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
late final _provider = VerificationProvider(
|
||||
context.read<VerificationService>(),
|
||||
);
|
||||
final _emailFocus = FocusNode();
|
||||
String? _recipient;
|
||||
bool _initialized = false;
|
||||
bool _lastWasSend = false;
|
||||
bool _validated = false;
|
||||
@@ -56,6 +60,7 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
void didUpdateWidget(covariant VerificationPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.token == widget.token) return;
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
_provider.reset();
|
||||
_lastWasSend = false;
|
||||
_validated = false;
|
||||
@@ -68,7 +73,12 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
void _send() {
|
||||
if (_provider.state.isLoading) return;
|
||||
_validated = true;
|
||||
if (!(_form.currentState?.validate() ?? false)) return;
|
||||
if (_provider.state.status != VerificationStatus.sent &&
|
||||
!(_form.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
_recipient = _email.text.trim();
|
||||
FocusScope.of(context).unfocus();
|
||||
_lastWasSend = true;
|
||||
_provider.send(
|
||||
email: _email.text,
|
||||
@@ -76,10 +86,20 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _changeEmail() {
|
||||
_lastWasSend = true;
|
||||
_validated = false;
|
||||
_provider.reset();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _emailFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider.dispose();
|
||||
_email.dispose();
|
||||
_emailFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -99,21 +119,32 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
].contains(status);
|
||||
final verified = status == VerificationStatus.verified;
|
||||
final demo =
|
||||
_provider.service is DummyVerificationService || state.isSimulated;
|
||||
(_provider.service is SimulationInfo &&
|
||||
(_provider.service as SimulationInfo).isSimulated) ||
|
||||
state.isSimulated;
|
||||
final sent = status == VerificationStatus.sent;
|
||||
final retryConfirmation =
|
||||
status == VerificationStatus.error &&
|
||||
!_lastWasSend &&
|
||||
widget.token != null;
|
||||
final title = switch (status) {
|
||||
VerificationStatus.sent => l.checkInbox,
|
||||
VerificationStatus.sent => demo ? l.demoSentHeading : l.checkInbox,
|
||||
VerificationStatus.sending => l.verificationSending,
|
||||
VerificationStatus.checking => l.verificationChecking,
|
||||
VerificationStatus.verified => l.verificationVerified,
|
||||
VerificationStatus.verified =>
|
||||
demo ? l.demoVerifiedHeading : l.verificationVerified,
|
||||
VerificationStatus.expired => l.verificationExpired,
|
||||
VerificationStatus.invalid => l.verificationInvalid,
|
||||
VerificationStatus.error => l.verificationInternal,
|
||||
VerificationStatus.idle =>
|
||||
widget.token != null ? l.verificationChecking : l.verificationRequest,
|
||||
widget.token != null && !_lastWasSend
|
||||
? l.verificationChecking
|
||||
: l.verificationRequest,
|
||||
};
|
||||
final body = switch (status) {
|
||||
VerificationStatus.sent => l.verificationSent,
|
||||
VerificationStatus.verified => l.verificationVerifiedBody,
|
||||
VerificationStatus.sent => demo ? l.demoSentBody : l.verificationSent,
|
||||
VerificationStatus.verified =>
|
||||
demo ? l.demoVerifiedBody : l.verificationVerifiedBody,
|
||||
VerificationStatus.expired => l.verificationExpiredBody,
|
||||
VerificationStatus.invalid => l.verificationInvalidBody,
|
||||
VerificationStatus.error => l.verificationInternalBody,
|
||||
@@ -174,9 +205,17 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
if (demo) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l.verificationDemo,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Semantics(
|
||||
liveRegion: true,
|
||||
header: true,
|
||||
child: Text(
|
||||
title,
|
||||
key: ValueKey('verification-${status.name}'),
|
||||
@@ -192,20 +231,62 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (status == VerificationStatus.sent) ...[
|
||||
const SizedBox(height: 16),
|
||||
if (sent) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l.verificationHint,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
l.verificationRecipient,
|
||||
style: theme.textTheme.labelLarge,
|
||||
),
|
||||
Text(
|
||||
_recipient ?? _email.text,
|
||||
key: const ValueKey('verification-recipient'),
|
||||
),
|
||||
TextButton(
|
||||
key: const ValueKey('verification-change-email'),
|
||||
onPressed: _changeEmail,
|
||||
child: Text(l.verificationChangeEmail),
|
||||
),
|
||||
if (demo)
|
||||
FilledButton(
|
||||
key: const ValueKey('demo-confirmation'),
|
||||
onPressed: () => context.goNamed(
|
||||
AppRoutes.verifyEmail,
|
||||
queryParameters: {'token': 'demo-valid'},
|
||||
),
|
||||
child: Text(
|
||||
l.verificationDemoLink,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
key: const ValueKey('verification-resend'),
|
||||
onPressed: _send,
|
||||
child: Text(
|
||||
demo ? l.demoResend : l.verificationResend,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (!demo) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l.verificationHint,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
if (!verified && status != VerificationStatus.checking) ...[
|
||||
if (!verified &&
|
||||
!sent &&
|
||||
!retryConfirmation &&
|
||||
status != VerificationStatus.checking) ...[
|
||||
const SizedBox(height: 28),
|
||||
Form(
|
||||
key: _form,
|
||||
child: TextFormField(
|
||||
key: const ValueKey('verification-email'),
|
||||
controller: _email,
|
||||
focusNode: _emailFocus,
|
||||
autocorrect: false,
|
||||
enabled: !state.isLoading,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
@@ -227,46 +308,28 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
child: Text(
|
||||
state.isLoading
|
||||
? l.authWorking
|
||||
: status == VerificationStatus.sent
|
||||
? l.verificationResend
|
||||
: l.verificationRequest,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (status == VerificationStatus.error &&
|
||||
!_lastWasSend &&
|
||||
widget.token != null) ...[
|
||||
if (retryConfirmation) ...[
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
FilledButton(
|
||||
key: const ValueKey('verification-retry'),
|
||||
onPressed: () => _provider.confirm(widget.token!),
|
||||
child: Text(l.verificationRetry),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _changeEmail,
|
||||
child: Text(l.verificationRequest),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => context.goNamed(AppRoutes.login),
|
||||
child: Text(l.verificationBack),
|
||||
),
|
||||
if (demo) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l.verificationDemo,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (status == VerificationStatus.sent)
|
||||
TextButton(
|
||||
key: const ValueKey('demo-confirmation'),
|
||||
onPressed: () => context.goNamed(
|
||||
AppRoutes.verifyEmail,
|
||||
queryParameters: {'token': 'demo-valid'},
|
||||
),
|
||||
child: Text(l.verificationDemoLink),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user