Files
sxchnge/frontend/app/lib/destinations/bank.dart

226 lines
7.0 KiB
Dart

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,
),
],
);
}
}