293 lines
12 KiB
Dart
293 lines
12 KiB
Dart
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<DestinationFormPage> createState() => _DestinationFormPageState();
|
|
}
|
|
|
|
class _DestinationFormPageState extends State<DestinationFormPage> {
|
|
late final p = DestinationFormProvider(
|
|
context.read<DestinationsProvider>().service,
|
|
context.read<AnalyticsService>(),
|
|
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<void> 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<DestinationsProvider>().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<DestinationType>(
|
|
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<String>(
|
|
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<String>(
|
|
value: 'demo-card-ready',
|
|
title: Text(l.dCardReady),
|
|
),
|
|
RadioListTile<String>(
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|