import 'package:sxchnge_analytics/sxchnge_analytics.dart'; import '../models/auth.dart'; import '../models/two_factor.dart'; import '../models/verification.dart'; import 'contracts.dart'; import 'guarded_login.dart'; import 'verification.dart'; /// Decorates domain services, so widget rebuilds never produce analytics events. class InstrumentedAuthServices implements PendingLoginService, 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 _run( JourneyKind kind, JourneyStep step, Future Function() action, bool Function(T) simulated, { bool terminal = false, bool Function(T)? terminalWhen, 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: terminalWhen?.call(result) ?? terminal, ); return result; } catch (error) { final failure = switch (error) { AuthException(code: final code) => switch (code) { AuthError.accountNotVerified => JourneyFailure.invalid, AuthError.invalidCredentials => JourneyFailure.invalidCredentials, AuthError.emailInUse => JourneyFailure.emailInUse, AuthError.unavailable => JourneyFailure.unavailable, AuthError.signupNeedsAssistance => JourneyFailure.unavailable, AuthError.unexpected => JourneyFailure.unexpected, AuthError.factorLocked || AuthError.factorCooldown => JourneyFailure.unavailable, AuthError.factorRequired => JourneyFailure.invalid, }, VerificationException(code: final code) => switch (code) { VerificationError.expired => JourneyFailure.expired, VerificationError.invalid => JourneyFailure.invalid, VerificationError.internal => JourneyFailure.internal, VerificationError.needsAssistance => JourneyFailure.unavailable, }, FactorException(code: FactorError.expired) => JourneyFailure.expired, FactorException() => JourneyFailure.unavailable, _ => JourneyFailure.unexpected, }; recorder.finish(operation, failure: failure); rethrow; } } bool _simulation(Object service) => service is SimulationInfo && service.isSimulated; @override Future login(LoginRequest request) => _run( JourneyKind.login, JourneyStep.login, () => loginService.login(request), (r) => r.isSimulated, terminalWhen: (r) => r.challenge == null, simulation: _simulation(loginService), ); @override Future completeLogin( FactorChallenge challenge, FactorGrant grant, ) => _run( JourneyKind.login, JourneyStep.login, () { final service = loginService; if (service is! PendingLoginService) throw const AuthException(AuthError.unavailable); return service.completeLogin(challenge, grant); }, (r) => r.isSimulated, terminal: true, simulation: _simulation(loginService), ); @override Future signup(SignupRequest request) => _run( JourneyKind.signup, JourneyStep.signup, () => signupService.signup(request), (r) => r.isSimulated, simulation: _simulation(signupService), ); @override Future 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 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, ); }