import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:sxchnge_analytics/sxchnge_analytics.dart'; import 'package:sxchnge_wallet/sxchnge_wallet.dart'; import 'bank.dart'; import 'copy.dart'; import 'crypto.dart'; import 'nickname.dart'; import '../generated/i18n/app_localizations.dart'; class DestinationFormPage extends StatefulWidget { const DestinationFormPage({ super.key, this.payoutContext, this.initial, this.replacing, this.restoring, }); final PayoutContext? payoutContext; final DestinationDetails? initial; final String? replacing; final SavedReference? restoring; @override State createState() => _DestinationFormPageState(); } class _DestinationFormPageState extends State { late final p = DestinationFormProvider( context.read().service, context.read(), context: widget.payoutContext, initial: widget.initial, replacing: widget.replacing, restoring: widget.restoring, ); late DestinationType? type = widget.initial?.type; final nickname = TextEditingController(); final focus = FocusNode(); Future proceed({ bool withoutSaving = false, bool existing = false, }) async { if (existing) { await p.useExisting(); } else { await p.proceed(withoutSaving: withoutSaving); } if (!mounted) return; if (p.result != null) { final l = AppLocalizations.of(context)!; if (p.saved?.outcome == SaveOutcome.created || widget.restoring != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( widget.restoring == null ? l.dSaved : l.dAuthorizationUpdated, ), ), ); } Navigator.pop(context, p.result); return; } focus.requestFocus(); } @override void dispose() { p.dispose(); nickname.dispose(); focus.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; final caps = context.watch().capabilities; return ListenableBuilder( listenable: p, builder: (context, _) => PopScope( canPop: !p.busy, child: Scaffold( appBar: AppBar(title: Text(l.dAdd)), body: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (caps?.simulated == true) ...[ Text(l.dDemo), const SizedBox(height: 16), ], Text(l.dOwn), const SizedBox(height: 20), if (caps == null || caps.types.isEmpty) Text(l.dNoCapabilities) else ...[ DropdownButtonFormField( initialValue: caps.types.contains(type) ? type : null, isExpanded: true, decoration: InputDecoration(labelText: l.dType), items: [ for (final t in caps.types) DropdownMenuItem( value: t, child: Text( destinationType(l, t), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], onChanged: p.busy || p.uncertain || widget.restoring != null ? null : (v) { setState(() => type = v); p.change(null); }, ), const SizedBox(height: 20), AbsorbPointer( absorbing: p.busy || p.uncertain, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (type == DestinationType.crypto) CryptoFields( capabilities: caps, initial: widget.initial is CryptoDestination ? widget.initial as CryptoDestination : null, errorField: p.errorField, onChanged: p.change, ), if (type == DestinationType.russianBank) BankFields( initial: widget.initial is RussianBankDestination ? widget.initial as RussianBankDestination : null, errorField: p.errorField, onChanged: p.change, ), if (type == DestinationType.card && caps.simulated) ...[ Text(l.dCardDemo), const SizedBox(height: 16), RadioGroup( groupValue: p.details is CardDestination ? (p.details as CardDestination).capture : null, onChanged: (value) { if (value != null) { p.change( CardDestination( capture: value, brand: 'Demo card', last4: '4242', expiryMonth: 12, expiryYear: 2030, ), ); } }, child: Column( children: [ RadioListTile( value: 'demo-card-ready', title: Text(l.dCardReady), ), RadioListTile( value: 'demo-card-recover', title: Text(l.dCardRecover), ), ], ), ), ], if (type == DestinationType.card && !caps.simulated) Text(l.dNoCapabilities), if (widget.payoutContext != null) ...[ const SizedBox(height: 16), CheckboxListTile( contentPadding: EdgeInsets.zero, value: p.save, onChanged: (v) => p.preferences(save: v), title: Text(l.dSaveFuture), ), ], if (widget.restoring == null && (widget.payoutContext == null || p.save)) ...[ const SizedBox(height: 16), DestinationNickname( controller: nickname, enabled: !p.busy && !p.uncertain, onChanged: (v) => p.preferences(nickname: v), ), ], ], ), ), const SizedBox(height: 16), Focus( focusNode: focus, skipTraversal: true, child: Semantics( liveRegion: true, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (p.error != null) Text( p.uncertain ? l.dUncertain : p.errorField != null ? l.dInvalid : '${p.savingFailed ? '${l.dSaveFailed}\n' : ''}${destinationIssue(l, p.error!)}', key: const ValueKey('destination-error'), ), if (p.eligibility != null && !p.eligibility!.eligible) Text( '${p.saved != null ? '${l.dSavedIneligible}\n' : ''}${eligibilityText(l, p.eligibility!)}', ), if (p.saved?.outcome == SaveOutcome.duplicate) ...[ Text(l.dDuplicate), TextButton( onPressed: p.busy ? null : () => proceed(existing: true), child: Text(l.dUseExisting), ), ], if (p.saved?.outcome == SaveOutcome.created && p.result == null) Text(l.dSaved), ], ), ), ), if (p.busy) const LinearProgressIndicator(), if (p.uncertain) TextButton( onPressed: p.busy ? null : () => proceed(), child: Text(l.dCheckSave), ), if (p.savingFailed && widget.payoutContext != null) TextButton( onPressed: p.busy ? null : () => proceed(withoutSaving: true), child: Text(l.dWithoutSave), ), if (p.save && widget.payoutContext != null) Text(l.dSaveHelp), const SizedBox(height: 12), FilledButton( key: const ValueKey('destination-continue'), onPressed: p.busy || p.details == null ? null : () => proceed(), child: Text( widget.restoring != null ? l.dUpdateAuthorization : widget.payoutContext == null ? l.dSave : p.error != null && !p.uncertain ? l.dRetrySave : l.dContinue, ), ), ], ], ), ), ), ), ), ), ); } }