Add saved payout destination UX and service contracts (#54)
This commit is contained in:
1 parent
a5689876ff
commit
24af794112
49 files changed
+4687
-62
No files matched your search
@@ -0,0 +1,225 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
||||
|
||||
import 'paste.dart';
|
||||
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
class BankFields extends StatefulWidget {
|
||||
const BankFields({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
this.initial,
|
||||
this.errorField,
|
||||
});
|
||||
final ValueChanged<DestinationDetails?> onChanged;
|
||||
final RussianBankDestination? initial;
|
||||
final String? errorField;
|
||||
@override
|
||||
State<BankFields> createState() => _BankFieldsState();
|
||||
}
|
||||
|
||||
class _BankFieldsState extends State<BankFields> {
|
||||
late final first = TextEditingController(text: widget.initial?.firstName);
|
||||
late final last = TextEditingController(text: widget.initial?.lastName);
|
||||
late final patronymic = TextEditingController(
|
||||
text: widget.initial?.patronymic,
|
||||
);
|
||||
late final account = TextEditingController(text: widget.initial?.account);
|
||||
final query = TextEditingController();
|
||||
late final search = BankSearchProvider(
|
||||
context.read<DestinationsProvider>().service,
|
||||
);
|
||||
late Bank? bank = widget.initial?.bank;
|
||||
String? pastedHolder;
|
||||
bool conflict = false;
|
||||
Map<String, String> pasted = {};
|
||||
bool get mismatch =>
|
||||
bank != null &&
|
||||
((pasted['bank'] != null && pasted['bank'] != bank!.name) ||
|
||||
(pasted['correspondent'] != null &&
|
||||
pasted['correspondent'] != bank!.correspondent));
|
||||
void changed() => widget.onChanged(
|
||||
bank == null
|
||||
? null
|
||||
: RussianBankDestination(
|
||||
firstName: first.text,
|
||||
lastName: last.text,
|
||||
patronymic: patronymic.text,
|
||||
bank: bank!,
|
||||
account: account.text,
|
||||
),
|
||||
);
|
||||
Future<void> paste() async {
|
||||
final parsed = await showDialog<BankPaste>(
|
||||
context: context,
|
||||
builder: (_) => const BankPasteDialog(),
|
||||
);
|
||||
if (!mounted || parsed == null) return;
|
||||
for (final entry in {
|
||||
'firstName': first,
|
||||
'lastName': last,
|
||||
'patronymic': patronymic,
|
||||
'account': account,
|
||||
}.entries) {
|
||||
if (parsed.conflicts.contains(entry.key)) {
|
||||
entry.value.clear();
|
||||
} else if (parsed.values[entry.key] case final String value) {
|
||||
entry.value.text = value;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
pasted = parsed.values;
|
||||
conflict = parsed.conflicts.isNotEmpty;
|
||||
pastedHolder = parsed.values['holder'];
|
||||
});
|
||||
if (parsed.conflicts.contains('bik')) {
|
||||
setState(() => bank = null);
|
||||
}
|
||||
if (parsed.values['bik'] case final String bik) {
|
||||
query.text = bik;
|
||||
search.search(bik);
|
||||
setState(() => bank = null);
|
||||
}
|
||||
changed();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in [first, last, patronymic, account, query]) {
|
||||
c.dispose();
|
||||
}
|
||||
search.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget field(
|
||||
String label,
|
||||
TextEditingController c, {
|
||||
String? id,
|
||||
bool numeric = false,
|
||||
String? error,
|
||||
}) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: TextField(
|
||||
key: id == null ? null : ValueKey(id),
|
||||
controller: c,
|
||||
keyboardType: numeric ? TextInputType.number : TextInputType.name,
|
||||
decoration: InputDecoration(labelText: label, errorText: error),
|
||||
onChanged: (_) => changed(),
|
||||
),
|
||||
);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(l.dHolder, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, b) {
|
||||
final a = field(
|
||||
l.dFirst,
|
||||
first,
|
||||
id: 'destination-first',
|
||||
error: widget.errorField == 'holder' ? l.dInvalid : null,
|
||||
);
|
||||
final z = field(l.dLast, last, id: 'destination-last');
|
||||
return b.maxWidth >= 480
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: a),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: z),
|
||||
],
|
||||
)
|
||||
: Column(children: [a, z]);
|
||||
},
|
||||
),
|
||||
field(l.dPatronymic, patronymic),
|
||||
if (pastedHolder != null) SelectableText(pastedHolder!),
|
||||
if (conflict) Text(l.dPasteConflict),
|
||||
Text(l.dBankDetails, style: Theme.of(context).textTheme.titleMedium),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: paste,
|
||||
icon: const Icon(Icons.content_paste),
|
||||
label: Text(l.dPaste),
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: search,
|
||||
builder: (context, _) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (bank == null) ...[
|
||||
TextField(
|
||||
controller: query,
|
||||
decoration: InputDecoration(labelText: l.dBankSearch),
|
||||
onChanged: search.search,
|
||||
),
|
||||
if (search.loading) const LinearProgressIndicator(),
|
||||
for (final b in search.results)
|
||||
ListTile(
|
||||
title: Text(b.name),
|
||||
subtitle: Text('${b.bik} · ${b.city}'),
|
||||
onTap: () {
|
||||
setState(() => bank = b);
|
||||
changed();
|
||||
},
|
||||
),
|
||||
if (search.error != null)
|
||||
Text(
|
||||
search.error == DestinationIssue.unavailable
|
||||
? l.dBankFailure
|
||||
: l.dBankUnknown,
|
||||
),
|
||||
if (search.error == DestinationIssue.unavailable)
|
||||
TextButton(
|
||||
onPressed: () => search.search(query.text),
|
||||
child: Text(l.wRefresh),
|
||||
),
|
||||
] else ...[
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(bank!.name),
|
||||
subtitle: Text('${l.dBik} ${bank!.bik}'),
|
||||
trailing: TextButton(
|
||||
onPressed: () {
|
||||
setState(() => bank = null);
|
||||
changed();
|
||||
},
|
||||
child: Text(l.dChangeBank),
|
||||
),
|
||||
),
|
||||
ExpansionTile(
|
||||
title: Text(l.dBankDetails),
|
||||
children: [
|
||||
SelectableText(
|
||||
'${l.dCorrespondent}: ${bank!.correspondent}',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!bank!.supported) Text(l.dUnsupported),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
field(
|
||||
l.dAccount,
|
||||
account,
|
||||
id: 'destination-account',
|
||||
numeric: true,
|
||||
error: widget.errorField == 'account' ? l.dInvalid : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user