Files
sxchnge/frontend/packages/sxchnge_auth/lib/src/services/instrumented.dart
T

143 lines
4.3 KiB
Dart

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<T> _run<T>(
JourneyKind kind,
JourneyStep step,
Future<T> 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.invalidCredentials => JourneyFailure.invalidCredentials,
AuthError.emailInUse => JourneyFailure.emailInUse,
AuthError.unavailable => 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,
},
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<AuthResult> login(LoginRequest request) => _run(
JourneyKind.login,
JourneyStep.login,
() => loginService.login(request),
(r) => r.isSimulated,
terminalWhen: (r) => r.challenge == null,
simulation: _simulation(loginService),
);
@override
Future<AuthResult> 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<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,
);
}