import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:sxchnge_analytics/sxchnge_analytics.dart'; import '../models/identity.dart'; import '../services/contracts.dart'; import '../services/dummy.dart'; class IdentityProvider extends ChangeNotifier { IdentityProvider( this.service, this.photos, this.journeys, { this.onStatusChanged, }); final Future Function()? onStatusChanged; final IdentityService service; final PhotoService photos; final JourneyRecorder journeys; Identity? record; Photo? passport; Photo? selfie; IdentityProfile profile = const IdentityProfile(); KycError? error; PhotoKind? photoError; bool busy = false; bool stale = false; bool editing = false; bool confirmed = false; int step = 0; bool _disposed = false; String? _key; bool _profileTouched = false; bool get hasDraft => passport != null || selfie != null || _profileTouched; bool get canSubmit => !busy && !stale && confirmed && profile.isComplete && passport != null && selfie != null && (record?.canSubmit ?? false); bool get isDemo => service.isSimulated; void _notify() { if (!_disposed) notifyListeners(); } JourneyOperation _begin(JourneyStep step) => journeys.begin(JourneyKind.kyc, step, simulated: isDemo); Future refresh() async { if (busy) return; busy = true; error = null; photoError = null; _notify(); final op = _begin(JourneyStep.kycStatus); try { final result = await service.fetch(); if (_disposed) return; record = result; if (!editing) profile = result.profile; stale = false; await onStatusChanged?.call(); if (_disposed) return; if (!result.canSubmit) { _clear(); editing = false; } journeys.finish(op, simulated: isDemo, terminal: true); } catch (e) { if (_disposed) return; stale = true; error = _error(e); journeys.finish(op, failure: JourneyFailure.unavailable, terminal: false); } busy = false; _notify(); } void start() { if (busy || stale || !(record?.canSubmit ?? false)) return; editing = true; error = null; if (!hasDraft) { step = 0; confirmed = false; } final op = _begin(JourneyStep.kycPreparation); journeys.finish(op, simulated: isDemo); _notify(); } Future pick(PhotoKind kind) async { if (busy || !editing) return; busy = true; error = null; photoError = null; _notify(); final op = _begin( kind == PhotoKind.passport ? JourneyStep.kycPassport : JourneyStep.kycSelfie, ); try { final photo = await photos.pick(); if (_disposed) return; if (photo != null) { if (kind == PhotoKind.passport) { passport = photo; } else { selfie = photo; } confirmed = false; _key = null; } journeys.finish(op, simulated: isDemo, cancelled: photo == null); } catch (e) { if (_disposed) return; error = _error(e); photoError = kind; journeys.finish(op, failure: JourneyFailure.document); } busy = false; _notify(); } void remove(PhotoKind kind) { if (busy) return; if (kind == PhotoKind.passport) { passport = null; } else { selfie = null; } confirmed = false; _key = null; _notify(); } void back() { if (!busy && step > 0) { step--; error = null; _notify(); } } void next() { if (busy || (step == 0 && !profile.isComplete) || (step == 0 && passport == null) || (step == 1 && selfie == null)) return; if (step < 2) step++; error = null; _notify(); } void updateProfile(IdentityProfile value) { if (busy || !editing) return; profile = value; _profileTouched = true; confirmed = false; _key = null; _notify(); } void confirm(bool value) { if (!busy) { confirmed = value; _notify(); } } Future submit() async { if (!canSubmit) return; busy = true; error = null; photoError = null; _notify(); _key ??= List.generate( 16, (_) => Random.secure().nextInt(256).toRadixString(16).padLeft(2, '0'), ).join(); final op = _begin(JourneyStep.kycSubmission); try { final result = await service.submit( profile: profile, passport: passport!, selfie: selfie!, idempotencyKey: _key!, ); if (_disposed) return; if (result.canSubmit) throw const KycException(KycError.conflict); record = result; editing = false; _clear(); await onStatusChanged?.call(); if (_disposed) return; journeys.finish(op, simulated: isDemo, terminal: true); } catch (e) { if (_disposed) return; error = _error(e); if (error == KycError.conflict || error == KycError.sessionExpired) stale = true; journeys.finish(op, failure: JourneyFailure.unavailable); } busy = false; _notify(); } Future demoReview(IdentityStatus status, {ReviewReason? reason}) async { if (busy || service is! DummyIdentityService) return; try { (service as DummyIdentityService).review(status, reason: reason); } on KycException catch (e) { error = e.code; _notify(); return; } await refresh(); } void exit() { if (busy) return; journeys.abandon(JourneyExit.navigation); _clear(); editing = false; error = null; _notify(); } void _clear() { _profileTouched = false; profile = const IdentityProfile(); passport = null; selfie = null; confirmed = false; _key = null; step = 0; } KycError _error(Object e) => e is KycException ? e.code : KycError.internal; @override void dispose() { _disposed = true; _clear(); journeys.dispose(); super.dispose(); } }