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
@@ -48,6 +48,7 @@ String fundsText(
|
||||
_ => l.wFailedOutgoing,
|
||||
};
|
||||
String walletError(AppLocalizations l, WalletError e) => switch (e) {
|
||||
WalletError.destinationChanged => l.dQuoteChanged,
|
||||
WalletError.invalidAmount => l.wInvalidAmount,
|
||||
WalletError.insufficientFunds => l.wInsufficient,
|
||||
WalletError.limitExceeded => l.wLimitError,
|
||||
|
||||
@@ -106,6 +106,15 @@ class SettingsPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.account_balance_outlined),
|
||||
title: Text(l.dTitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => context.push('/settings/payout-methods'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SecuritySettings(),
|
||||
const SizedBox(height: 20),
|
||||
TextButton(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -6,6 +8,8 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
||||
|
||||
import '../destinations/details.dart';
|
||||
|
||||
import 'copy.dart';
|
||||
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
@@ -49,13 +53,23 @@ class TransferSummary extends StatelessWidget {
|
||||
q.request.kind == TransferKind.payout
|
||||
? l.wRecipientGets
|
||||
: l.wCredit,
|
||||
money(context, b, q.credit),
|
||||
q.payoutTerms == null
|
||||
? money(context, b, q.credit)
|
||||
: '${NumberFormat.decimalPatternDigits(locale: Localizations.localeOf(context).toLanguageTag(), decimalDigits: q.payoutTerms!.precision).format(q.payoutTerms!.amount / pow(10, q.payoutTerms!.precision))} ${q.payoutTerms!.assetLabel}',
|
||||
large: true,
|
||||
),
|
||||
if (q.payoutTerms != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(l.dTiming(q.payoutTerms!.expectedSeconds)),
|
||||
if (p.service.isSimulated) Text(l.dDemoTerms),
|
||||
],
|
||||
if (q.request.kind == TransferKind.payout) ...[
|
||||
const Divider(height: 32),
|
||||
Text(l.wRecipient),
|
||||
SelectableText(q.request.recipient),
|
||||
if (q.payoutSnapshot != null)
|
||||
DestinationDetailsView(details: q.payoutSnapshot!)
|
||||
else
|
||||
SelectableText(q.request.recipient),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
||||
|
||||
import '../security/challenge.dart';
|
||||
import '../destinations/list.dart';
|
||||
|
||||
import 'copy.dart';
|
||||
import 'summary.dart';
|
||||
@@ -32,7 +33,7 @@ class TransferPageState extends State<TransferPage> {
|
||||
source: widget.source,
|
||||
);
|
||||
final _amount = TextEditingController();
|
||||
final _recipient = TextEditingController();
|
||||
|
||||
final _focus = FocusNode();
|
||||
final _content = GlobalKey();
|
||||
Timer? _timer;
|
||||
@@ -41,7 +42,13 @@ class TransferPageState extends State<TransferPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _provider.addListener(_changed);
|
||||
if (mounted) {
|
||||
_provider.addListener(_changed);
|
||||
final destinations = context.read<DestinationsProvider>();
|
||||
if (destinations.capabilities == null && !destinations.loading) {
|
||||
unawaited(destinations.refresh());
|
||||
}
|
||||
}
|
||||
});
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (mounted && _provider.quote != null) setState(() {});
|
||||
@@ -118,7 +125,7 @@ class TransferPageState extends State<TransferPage> {
|
||||
_provider.removeListener(_changed);
|
||||
_provider.dispose();
|
||||
_amount.dispose();
|
||||
_recipient.dispose();
|
||||
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -134,14 +141,16 @@ class TransferPageState extends State<TransferPage> {
|
||||
final accounts = wallet.data?.accounts ?? [];
|
||||
final source = wallet.account(p.source);
|
||||
final live = !wallet.service.isSimulated;
|
||||
final destinations = context.watch<DestinationsProvider>();
|
||||
final legacyPayout =
|
||||
live &&
|
||||
!(destinations.capabilities?.types.isNotEmpty ?? false) &&
|
||||
destinations.methods.isEmpty;
|
||||
final expiresIn =
|
||||
p.quote?.expiresAt.difference(DateTime.now()).inSeconds ?? 0;
|
||||
final unavailableMethod =
|
||||
live &&
|
||||
((widget.kind == TransferKind.payout &&
|
||||
(source?.payoutMethod.isEmpty ?? true)) ||
|
||||
(widget.kind == TransferKind.topUp &&
|
||||
!(source?.canFund ?? false)));
|
||||
((widget.kind == TransferKind.topUp && !(source?.canFund ?? false)));
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
@@ -235,7 +244,11 @@ class TransferPageState extends State<TransferPage> {
|
||||
FilledButton(
|
||||
key: const ValueKey('transfer-confirm'),
|
||||
onPressed: p.busy ? null : _confirm,
|
||||
child: Text(l.wConfirm),
|
||||
child: Text(
|
||||
widget.kind == TransferKind.payout
|
||||
? '${l.wPayout} ${money(context, source!, p.quote!.debit)}'
|
||||
: l.wConfirm,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: p.busy ? null : p.edit,
|
||||
@@ -316,23 +329,66 @@ class TransferPageState extends State<TransferPage> {
|
||||
),
|
||||
if (widget.kind == TransferKind.payout) ...[
|
||||
const SizedBox(height: 20),
|
||||
if (live && source?.payoutMethod.isNotEmpty == true)
|
||||
InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l.wRecipient,
|
||||
if (legacyPayout) ...[
|
||||
if (source?.payoutMethod.isNotEmpty == true)
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey(
|
||||
'choose-configured-destination',
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.account_balance_outlined,
|
||||
),
|
||||
label: Text(source!.payoutLabel),
|
||||
onPressed: p.busy
|
||||
? null
|
||||
: () => p.change(
|
||||
recipient: source.payoutMethod,
|
||||
payoutLabel: source.payoutLabel,
|
||||
),
|
||||
),
|
||||
child: Text(source!.payoutLabel),
|
||||
),
|
||||
if (!live)
|
||||
TextField(
|
||||
key: const ValueKey('transfer-recipient'),
|
||||
controller: _recipient,
|
||||
enabled: !p.busy,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.wRecipient,
|
||||
hintText: l.wRecipientHint,
|
||||
if (source?.payoutMethod.isEmpty ?? true)
|
||||
Text(l.wNoPayoutMethod),
|
||||
] else
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey(
|
||||
'choose-payout-destination',
|
||||
),
|
||||
onChanged: (v) => p.change(recipient: v),
|
||||
icon: const Icon(
|
||||
Icons.account_balance_wallet_outlined,
|
||||
),
|
||||
label: Text(
|
||||
p.payoutDestination == null
|
||||
? l.dSelect
|
||||
: p.payoutLabel,
|
||||
),
|
||||
onPressed: p.busy
|
||||
? null
|
||||
: () async {
|
||||
final selected =
|
||||
await Navigator.push<
|
||||
DestinationInput
|
||||
>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
DestinationsPage(
|
||||
payoutContext:
|
||||
PayoutContext(
|
||||
p.source,
|
||||
amount: source
|
||||
?.parse(
|
||||
p.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!context.mounted ||
|
||||
selected == null) {
|
||||
return;
|
||||
}
|
||||
await p.selectPayout(selected);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (unavailableMethod) ...[
|
||||
@@ -346,7 +402,13 @@ class TransferPageState extends State<TransferPage> {
|
||||
const SizedBox(height: 28),
|
||||
FilledButton(
|
||||
key: const ValueKey('transfer-review'),
|
||||
onPressed: p.busy || unavailableMethod
|
||||
onPressed:
|
||||
p.busy ||
|
||||
unavailableMethod ||
|
||||
(widget.kind == TransferKind.payout &&
|
||||
(legacyPayout
|
||||
? p.recipient.isEmpty
|
||||
: p.payoutDestination == null))
|
||||
? null
|
||||
: p.review,
|
||||
child: Text(l.wReview),
|
||||
|
||||
Reference in new issue
Block a user