253 lines
5.9 KiB
Dart
253 lines
5.9 KiB
Dart
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<void> 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<void> 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<void> 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<void> 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<void> 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();
|
|
}
|
|
}
|