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
@@ -0,0 +1,117 @@
|
||||
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
|
||||
import '../models/auth.dart';
|
||||
import '../models/verification.dart';
|
||||
import 'contracts.dart';
|
||||
import 'verification.dart';
|
||||
|
||||
/// Decorates domain services, so widget rebuilds never produce analytics events.
|
||||
class InstrumentedAuthServices
|
||||
implements
|
||||
LoginService,
|
||||
SignupService,
|
||||
VerificationService,
|
||||
SimulationInfo {
|
||||
InstrumentedAuthServices({
|
||||
required this.recorder,
|
||||
required this.loginService,
|
||||
required this.signupService,
|
||||
required this.verificationService,
|
||||
});
|
||||
final JourneyRecorder recorder;
|
||||
final LoginService loginService;
|
||||
final SignupService signupService;
|
||||
final VerificationService verificationService;
|
||||
@override
|
||||
bool get isSimulated =>
|
||||
verificationService is SimulationInfo &&
|
||||
(verificationService as SimulationInfo).isSimulated;
|
||||
|
||||
Future<T> _run<T>(
|
||||
JourneyKind kind,
|
||||
JourneyStep step,
|
||||
Future<T> Function() action,
|
||||
bool Function(T) simulated, {
|
||||
bool terminal = false,
|
||||
bool onboarding = false,
|
||||
bool simulation = false,
|
||||
}) async {
|
||||
final operation = recorder.begin(
|
||||
kind,
|
||||
step,
|
||||
onboarding: onboarding,
|
||||
simulated: simulation,
|
||||
);
|
||||
try {
|
||||
final result = await action();
|
||||
recorder.finish(
|
||||
operation,
|
||||
simulated: simulated(result),
|
||||
terminal: terminal,
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
final failure = switch (error) {
|
||||
AuthException(code: final code) => switch (code) {
|
||||
AuthError.invalidCredentials => JourneyFailure.invalidCredentials,
|
||||
AuthError.emailInUse => JourneyFailure.emailInUse,
|
||||
AuthError.unavailable => JourneyFailure.unavailable,
|
||||
AuthError.unexpected => JourneyFailure.unexpected,
|
||||
},
|
||||
VerificationException(code: final code) => switch (code) {
|
||||
VerificationError.expired => JourneyFailure.expired,
|
||||
VerificationError.invalid => JourneyFailure.invalid,
|
||||
VerificationError.internal => JourneyFailure.internal,
|
||||
},
|
||||
_ => JourneyFailure.unexpected,
|
||||
};
|
||||
recorder.finish(operation, failure: failure);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
bool _simulation(Object service) =>
|
||||
service is SimulationInfo && service.isSimulated;
|
||||
|
||||
@override
|
||||
Future<AuthResult> login(LoginRequest request) => _run(
|
||||
JourneyKind.login,
|
||||
JourneyStep.login,
|
||||
() => loginService.login(request),
|
||||
(r) => r.isSimulated,
|
||||
terminal: true,
|
||||
simulation: _simulation(loginService),
|
||||
);
|
||||
@override
|
||||
Future<AuthResult> signup(SignupRequest request) => _run(
|
||||
JourneyKind.signup,
|
||||
JourneyStep.signup,
|
||||
() => signupService.signup(request),
|
||||
(r) => r.isSimulated,
|
||||
simulation: _simulation(signupService),
|
||||
);
|
||||
@override
|
||||
Future<VerificationResult> send({
|
||||
required String email,
|
||||
required String locale,
|
||||
}) => _run(
|
||||
JourneyKind.verification,
|
||||
JourneyStep.emailDelivery,
|
||||
() => verificationService.send(email: email, locale: locale),
|
||||
(r) => r.isSimulated,
|
||||
onboarding: true,
|
||||
simulation: isSimulated,
|
||||
);
|
||||
@override
|
||||
Future<VerificationResult> confirm({required String token}) => _run(
|
||||
JourneyKind.verification,
|
||||
JourneyStep.emailConfirmation,
|
||||
() => token.trim().isEmpty
|
||||
? Future.error(const VerificationException(VerificationError.invalid))
|
||||
: verificationService.confirm(token: token),
|
||||
(r) => r.isSimulated,
|
||||
terminal: true,
|
||||
onboarding: true,
|
||||
simulation: isSimulated,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user