Add reusable two-factor verification for login and money operations (#32)

This commit is contained in:
Stephan D committed 2026-09-12 12:44:34 +02:00
1 parent b7a58b1a87
commit fb40f860d2
41 files changed
+2439 -43

No files matched your search

@@ -1,14 +1,16 @@
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
LoginService,
PendingLoginService,
SignupService,
VerificationService,
SimulationInfo {
@@ -33,6 +35,7 @@ class InstrumentedAuthServices
Future<T> Function() action,
bool Function(T) simulated, {
bool terminal = false,
bool Function(T)? terminalWhen,
bool onboarding = false,
bool simulation = false,
}) async {
@@ -47,7 +50,7 @@ class InstrumentedAuthServices
recorder.finish(
operation,
simulated: simulated(result),
terminal: terminal,
terminal: terminalWhen?.call(result) ?? terminal,
);
return result;
} catch (error) {
@@ -57,12 +60,17 @@ class InstrumentedAuthServices
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);
@@ -79,6 +87,23 @@ class InstrumentedAuthServices
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),
);