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,292 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user