277 lines
8.0 KiB
Dart
277 lines
8.0 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
|
|
|
import 'key.dart';
|
|
import 'models.dart';
|
|
import 'service.dart';
|
|
|
|
class DestinationFormProvider extends ChangeNotifier {
|
|
DestinationFormProvider(
|
|
this.service,
|
|
this.analytics, {
|
|
this.context,
|
|
this.initial,
|
|
this.replacing,
|
|
this.restoring,
|
|
}) : recorder = JourneyRecorder(analytics),
|
|
details = initial {
|
|
if (context == null) _startJourney();
|
|
}
|
|
final DestinationService service;
|
|
final AnalyticsService analytics;
|
|
final JourneyRecorder recorder;
|
|
final PayoutContext? context;
|
|
final DestinationDetails? initial;
|
|
final String? replacing;
|
|
final SavedReference? restoring;
|
|
DestinationDetails? details;
|
|
String nickname = '';
|
|
bool save = false, busy = false, _disposed = false, uncertain = false;
|
|
DestinationIssue? error;
|
|
String? errorField;
|
|
CreationResult? saved;
|
|
PayoutEligibility? eligibility;
|
|
DestinationInput? result;
|
|
String? _key;
|
|
bool _creationStarted = false;
|
|
bool _journeyStarted = false;
|
|
void _startJourney() {
|
|
if (_journeyStarted) return;
|
|
_journeyStarted = true;
|
|
final op = recorder.begin(
|
|
JourneyKind.destination,
|
|
JourneyStep.destinationDraft,
|
|
simulated: service.isSimulated,
|
|
);
|
|
recorder.finish(op, simulated: service.isSimulated);
|
|
}
|
|
|
|
bool savingFailed = false;
|
|
bool get hasDraft => details != null || nickname.isNotEmpty;
|
|
void change(DestinationDetails? value) {
|
|
if (busy || uncertain) return;
|
|
details = value;
|
|
error = null;
|
|
errorField = null;
|
|
eligibility = null;
|
|
result = null;
|
|
saved = null;
|
|
_key = null;
|
|
_creationStarted = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
void preferences({bool? save, String? nickname}) {
|
|
if (busy || uncertain) return;
|
|
if (save == true) _startJourney();
|
|
if (save == false && this.save) {
|
|
recorder.abandon(JourneyExit.modeChange);
|
|
_journeyStarted = false;
|
|
}
|
|
this.save = save ?? this.save;
|
|
this.nickname = nickname ?? this.nickname;
|
|
_key = null;
|
|
saved = null;
|
|
error = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> proceed({bool withoutSaving = false}) async {
|
|
if (busy || details == null) return;
|
|
if (withoutSaving && !uncertain) recorder.abandon(JourneyExit.modeChange);
|
|
if (uncertain && !withoutSaving) {
|
|
await reconcile();
|
|
return;
|
|
}
|
|
busy = true;
|
|
error = null;
|
|
errorField = null;
|
|
savingFailed = false;
|
|
if (!uncertain) _creationStarted = false;
|
|
notifyListeners();
|
|
try {
|
|
// Validation precedes both persistence and payout assessment.
|
|
final normalized = (await service.validate(details!)).details;
|
|
if (_disposed) return;
|
|
if (restoring != null) {
|
|
_key ??= destinationKey();
|
|
final bindingOperation = recorder.begin(
|
|
JourneyKind.destination,
|
|
JourneyStep.destinationBinding,
|
|
simulated: service.isSimulated,
|
|
);
|
|
final restored = await service.restoreCard(
|
|
restoring!.id,
|
|
restoring!.executionRevision,
|
|
normalized as CardDestination,
|
|
_key!,
|
|
);
|
|
recorder.finish(
|
|
bindingOperation,
|
|
terminal: true,
|
|
simulated: service.isSimulated,
|
|
);
|
|
if (!_disposed) result = restored.input;
|
|
} else if (context != null) {
|
|
eligibility = await service.assess(
|
|
OneTimeDestination(normalized),
|
|
context!,
|
|
);
|
|
if (_disposed) return;
|
|
}
|
|
if (restoring == null &&
|
|
(context == null || save) &&
|
|
!withoutSaving &&
|
|
saved == null) {
|
|
if (eligibility?.canSave == false)
|
|
throw const DestinationException(DestinationIssue.unsupported);
|
|
_key ??= destinationKey();
|
|
final op = recorder.begin(
|
|
JourneyKind.destination,
|
|
JourneyStep.destinationSave,
|
|
simulated: service.isSimulated,
|
|
);
|
|
_creationStarted = true;
|
|
try {
|
|
saved = await service.create(
|
|
normalized,
|
|
nickname,
|
|
_key!,
|
|
replaces: replacing,
|
|
);
|
|
_creationStarted = false;
|
|
if (_disposed) return;
|
|
recorder.finish(
|
|
op,
|
|
simulated: service.isSimulated,
|
|
terminal: true,
|
|
resolution: DestinationResolution.values.byName(
|
|
saved!.outcome.name,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
savingFailed = true;
|
|
recorder.finish(
|
|
op,
|
|
failure: JourneyFailure.unavailable,
|
|
simulated: service.isSimulated,
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
if (restoring != null) {
|
|
// Restored bindings never create or rename a saved destination.
|
|
} else if (context == null) {
|
|
if (saved?.outcome == SaveOutcome.created)
|
|
result = SavedReference(
|
|
saved!.destinationId,
|
|
(await service.get(saved!.destinationId)).executionRevision,
|
|
);
|
|
} else if (eligibility!.eligible) {
|
|
if (withoutSaving || saved == null) {
|
|
result = OneTimeDestination(normalized);
|
|
} else if (saved!.outcome == SaveOutcome.created) {
|
|
final d = await service.get(saved!.destinationId);
|
|
result = d.input;
|
|
}
|
|
// Duplicates need explicit Use existing method, never another create.
|
|
}
|
|
} on DestinationException catch (e) {
|
|
error = e.issue;
|
|
errorField = e.field;
|
|
uncertain =
|
|
_creationStarted &&
|
|
[
|
|
DestinationIssue.uncertain,
|
|
DestinationIssue.unavailable,
|
|
].contains(e.issue);
|
|
} catch (_) {
|
|
error = DestinationIssue.unavailable;
|
|
uncertain = _creationStarted;
|
|
}
|
|
if (_disposed) return;
|
|
busy = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> useExisting() async {
|
|
if (busy || saved == null) return;
|
|
error = null;
|
|
busy = true;
|
|
notifyListeners();
|
|
try {
|
|
final d = await service.get(saved!.destinationId);
|
|
if (d.removed) throw const DestinationException(DestinationIssue.removed);
|
|
if (context != null)
|
|
eligibility = await service.assess(d.input, context!);
|
|
if (context == null || eligibility!.eligible) result = d.input;
|
|
} on DestinationException catch (e) {
|
|
error = e.issue;
|
|
} catch (_) {
|
|
error = DestinationIssue.unavailable;
|
|
}
|
|
if (_disposed) return;
|
|
busy = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> reconcile() async {
|
|
if (busy || _key == null) return;
|
|
busy = true;
|
|
notifyListeners();
|
|
final op = recorder.begin(
|
|
JourneyKind.destination,
|
|
JourneyStep.destinationReconciliation,
|
|
simulated: service.isSimulated,
|
|
);
|
|
try {
|
|
final outcome = await service.creation(_key!);
|
|
if (_disposed) return;
|
|
saved = outcome;
|
|
recorder.finish(
|
|
op,
|
|
simulated: service.isSimulated,
|
|
terminal: outcome != null,
|
|
resolution: outcome == null
|
|
? null
|
|
: DestinationResolution.values.byName(outcome.outcome.name),
|
|
);
|
|
uncertain = false;
|
|
_creationStarted = false;
|
|
error = null;
|
|
// Absence is an authoritative lookup result, not a timeout inference.
|
|
if (outcome?.outcome == SaveOutcome.created && context == null) {
|
|
final d = await service.get(outcome!.destinationId);
|
|
if (d.removed)
|
|
throw const DestinationException(DestinationIssue.removed);
|
|
result = d.input;
|
|
}
|
|
} on DestinationException catch (e) {
|
|
recorder.finish(
|
|
op,
|
|
simulated: service.isSimulated,
|
|
failure: JourneyFailure.unavailable,
|
|
);
|
|
error = e.issue;
|
|
} catch (_) {
|
|
recorder.finish(
|
|
op,
|
|
simulated: service.isSimulated,
|
|
failure: JourneyFailure.unavailable,
|
|
);
|
|
error = DestinationIssue.unavailable;
|
|
}
|
|
if (_disposed) return;
|
|
busy = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
recorder.dispose(abandonActive: !uncertain);
|
|
super.dispose();
|
|
}
|
|
}
|