146 lines
4.6 KiB
Dart
146 lines
4.6 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.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<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,
|
|
);
|
|
}
|