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 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 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 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(); } }