309 lines
12 KiB
Dart
309 lines
12 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
|
|
|
import 'copy.dart';
|
|
import 'summary.dart';
|
|
|
|
import '../generated/i18n/app_localizations.dart';
|
|
|
|
class TransferPage extends StatefulWidget {
|
|
const TransferPage({super.key, required this.kind, this.source});
|
|
final TransferKind kind;
|
|
final String? source;
|
|
@override
|
|
State<TransferPage> createState() => TransferPageState();
|
|
}
|
|
|
|
class TransferPageState extends State<TransferPage> {
|
|
late final _provider = TransferProvider(
|
|
context.read<WalletProvider>(),
|
|
widget.kind,
|
|
source: widget.source,
|
|
);
|
|
final _amount = TextEditingController();
|
|
final _recipient = TextEditingController();
|
|
final _focus = FocusNode();
|
|
final _content = GlobalKey();
|
|
Timer? _timer;
|
|
bool _navigated = false;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) _provider.addListener(_changed);
|
|
});
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
if (mounted && _provider.quote != null) setState(() {});
|
|
});
|
|
}
|
|
|
|
void _changed() {
|
|
if (_provider.result != null && !_navigated) {
|
|
_navigated = true;
|
|
context.go('/activity/${_provider.result!.id}');
|
|
return;
|
|
}
|
|
if (!_provider.busy &&
|
|
(_provider.quote != null || _provider.error != null)) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted && _content.currentContext != null) {
|
|
Scrollable.ensureVisible(
|
|
_content.currentContext!,
|
|
duration: const Duration(milliseconds: 150),
|
|
);
|
|
_focus.requestFocus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<bool> allowExit() async {
|
|
if (_provider.result != null) return true;
|
|
if (_provider.busy || _provider.uncertain) return false;
|
|
if (!_provider.hasDraft) return true;
|
|
final l = AppLocalizations.of(context)!;
|
|
return await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l.wDiscardTitle),
|
|
content: Text(l.wDiscardBody),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: Text(l.wStay),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: Text(l.wClose),
|
|
),
|
|
],
|
|
),
|
|
) ??
|
|
false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
_provider.removeListener(_changed);
|
|
_provider.dispose();
|
|
_amount.dispose();
|
|
_recipient.dispose();
|
|
_focus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ListenableBuilder(
|
|
listenable: _provider,
|
|
builder: (context, _) {
|
|
final p = _provider;
|
|
final wallet = context.watch<WalletProvider>();
|
|
final l = AppLocalizations.of(context)!;
|
|
final t = Theme.of(context);
|
|
final accounts = wallet.data?.accounts ?? [];
|
|
final source = wallet.account(p.source);
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 640),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
p.quote == null
|
|
? kindText(l, widget.kind)
|
|
: l.wReviewTitle,
|
|
style: t.textTheme.displaySmall,
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: l.wClose,
|
|
onPressed: p.busy || p.uncertain
|
|
? null
|
|
: () => context.go('/accounts'),
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Focus(
|
|
key: _content,
|
|
focusNode: _focus,
|
|
skipTraversal: true,
|
|
includeSemantics: false,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (p.busy) ...[
|
|
const LinearProgressIndicator(),
|
|
const SizedBox(height: 12),
|
|
Semantics(liveRegion: true, child: Text(l.wWorking)),
|
|
],
|
|
if (p.error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: Semantics(
|
|
liveRegion: true,
|
|
child: Text(
|
|
walletError(l, p.error!),
|
|
key: const ValueKey('transfer-error'),
|
|
style: TextStyle(color: t.colorScheme.error),
|
|
),
|
|
),
|
|
),
|
|
if (p.noSubmission) Text(l.wNoSubmission),
|
|
if (p.uncertain) ...[
|
|
Text(l.wUncertain),
|
|
FilledButton(
|
|
onPressed: p.busy ? null : p.reconcile,
|
|
child: Text(l.wCheckOutcome),
|
|
),
|
|
] else if (p.quote != null) ...[
|
|
TransferSummary(quote: p.quote!),
|
|
const SizedBox(height: 16),
|
|
Text(p.expired ? l.wExpired : l.wQuoteNotice),
|
|
const SizedBox(height: 20),
|
|
if (p.expired)
|
|
FilledButton(
|
|
onPressed: p.busy ? null : p.review,
|
|
child: Text(l.wNewQuote),
|
|
)
|
|
else
|
|
FilledButton(
|
|
key: const ValueKey('transfer-confirm'),
|
|
onPressed: p.busy ? null : p.submit,
|
|
child: Text(l.wConfirm),
|
|
),
|
|
TextButton(
|
|
onPressed: p.busy ? null : p.edit,
|
|
child: Text(l.wEdit),
|
|
),
|
|
] else ...[
|
|
if (accounts.isEmpty) ...[
|
|
Text(wallet.loading ? l.wWorking : l.wEmptyAccounts),
|
|
TextButton(
|
|
onPressed: wallet.loading ? null : wallet.refresh,
|
|
child: Text(l.wRefresh),
|
|
),
|
|
] else ...[
|
|
DropdownButtonFormField<String>(
|
|
key: ValueKey('source-${p.source}'),
|
|
initialValue: accounts.any((a) => a.id == p.source)
|
|
? p.source
|
|
: null,
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
labelText: widget.kind == TransferKind.topUp
|
|
? l.wReceiveAccount
|
|
: l.wSource,
|
|
),
|
|
items: accounts
|
|
.map(
|
|
(a) => DropdownMenuItem(
|
|
value: a.id,
|
|
child: Text(a.currency),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: p.busy
|
|
? null
|
|
: (v) => p.change(source: v),
|
|
),
|
|
if (source != null) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'${l.wAvailable}: ${money(context, source, source.available)}',
|
|
),
|
|
],
|
|
if (widget.kind == TransferKind.exchange) ...[
|
|
const SizedBox(height: 20),
|
|
DropdownButtonFormField<String>(
|
|
key: ValueKey('destination-${p.destination}'),
|
|
initialValue: p.destination,
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
labelText: l.wDestination,
|
|
),
|
|
items: accounts
|
|
.map(
|
|
(a) => DropdownMenuItem(
|
|
value: a.id,
|
|
child: Text(a.currency),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: p.busy
|
|
? null
|
|
: (v) => p.change(destination: v),
|
|
),
|
|
],
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
key: const ValueKey('transfer-amount'),
|
|
controller: _amount,
|
|
enabled: !p.busy,
|
|
keyboardType: const TextInputType.numberWithOptions(
|
|
decimal: true,
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: l.wAmount,
|
|
suffixText: source?.currency,
|
|
),
|
|
onChanged: (v) => p.change(amount: v),
|
|
),
|
|
if (widget.kind == TransferKind.payout) ...[
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
key: const ValueKey('transfer-recipient'),
|
|
controller: _recipient,
|
|
enabled: !p.busy,
|
|
decoration: InputDecoration(
|
|
labelText: l.wRecipient,
|
|
hintText: l.wRecipientHint,
|
|
),
|
|
onChanged: (v) => p.change(recipient: v),
|
|
),
|
|
],
|
|
const SizedBox(height: 28),
|
|
FilledButton(
|
|
key: const ValueKey('transfer-review'),
|
|
onPressed: p.busy ? null : p.review,
|
|
child: Text(l.wReview),
|
|
),
|
|
if (p.error == WalletError.limitExceeded)
|
|
TextButton(
|
|
onPressed: () => context.push('/limits'),
|
|
child: Text(l.wLimits),
|
|
),
|
|
if (p.error == WalletError.insufficientFunds)
|
|
TextButton(
|
|
onPressed: () =>
|
|
context.push('/top-up', extra: p.source),
|
|
child: Text(l.wTopUp),
|
|
),
|
|
],
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|