mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-08 19:59:29 +00:00
desktop trade steps flow fade transition and state management updates
This commit is contained in:
parent
7641539bf7
commit
779bf20cc4
10 changed files with 584 additions and 566 deletions
|
@ -1,8 +1,9 @@
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
||||
|
||||
class IncompleteExchangeModel {
|
||||
class IncompleteExchangeModel extends ChangeNotifier {
|
||||
final String sendTicker;
|
||||
final String receiveTicker;
|
||||
|
||||
|
@ -15,12 +16,49 @@ class IncompleteExchangeModel {
|
|||
|
||||
final bool reversed;
|
||||
|
||||
String? recipientAddress;
|
||||
String? refundAddress;
|
||||
String? _recipientAddress;
|
||||
|
||||
String? rateId;
|
||||
String? get recipientAddress => _recipientAddress;
|
||||
|
||||
Trade? trade;
|
||||
set recipientAddress(String? recipientAddress) {
|
||||
if (_recipientAddress != recipientAddress) {
|
||||
_recipientAddress = recipientAddress;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String? _refundAddress;
|
||||
|
||||
String? get refundAddress => _refundAddress;
|
||||
|
||||
set refundAddress(String? refundAddress) {
|
||||
if (_refundAddress != refundAddress) {
|
||||
_refundAddress = refundAddress;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String? _rateId;
|
||||
|
||||
String? get rateId => _rateId;
|
||||
|
||||
set rateId(String? rateId) {
|
||||
if (_rateId != rateId) {
|
||||
_rateId = rateId;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Trade? _trade;
|
||||
|
||||
Trade? get trade => _trade;
|
||||
|
||||
set trade(Trade? trade) {
|
||||
if (_trade != trade) {
|
||||
_trade = trade;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
IncompleteExchangeModel({
|
||||
required this.sendTicker,
|
||||
|
@ -30,6 +68,6 @@ class IncompleteExchangeModel {
|
|||
required this.receiveAmount,
|
||||
required this.rateType,
|
||||
required this.reversed,
|
||||
this.rateId,
|
||||
});
|
||||
String? rateId,
|
||||
}) : _rateId = rateId;
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_op
|
|||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
|
||||
|
@ -1022,19 +1020,16 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
ref.read(exchangeSendFromWalletIdStateProvider.state).state =
|
||||
Tuple2(walletId!, coin!);
|
||||
if (isDesktop) {
|
||||
ref.read(ssss.state).state = model;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
return const DesktopDialog(
|
||||
maxWidth: 720,
|
||||
maxHeight: double.infinity,
|
||||
child: StepScaffold(
|
||||
step: 2,
|
||||
model: model,
|
||||
body: DesktopStep2(
|
||||
model: model,
|
||||
),
|
||||
initialStep: 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -1051,19 +1046,16 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
ref.read(exchangeSendFromWalletIdStateProvider.state).state = null;
|
||||
|
||||
if (isDesktop) {
|
||||
ref.read(ssss.state).state = model;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
return const DesktopDialog(
|
||||
maxWidth: 720,
|
||||
maxHeight: double.infinity,
|
||||
child: StepScaffold(
|
||||
step: 1,
|
||||
model: model,
|
||||
body: DesktopStep1(
|
||||
model: model,
|
||||
),
|
||||
initialStep: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,39 +1,190 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class StepScaffold extends StatefulWidget {
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart';
|
||||
import 'package:stackwallet/providers/exchange/exchange_provider.dart';
|
||||
import 'package:stackwallet/providers/global/trades_service_provider.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_response.dart';
|
||||
import 'package:stackwallet/services/notifications_api.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/fade_stack.dart';
|
||||
|
||||
final ssss = StateProvider<IncompleteExchangeModel?>((_) => null);
|
||||
|
||||
final desktopExchangeModelProvider =
|
||||
ChangeNotifierProvider<IncompleteExchangeModel?>(
|
||||
(ref) => ref.watch(ssss.state).state);
|
||||
|
||||
class StepScaffold extends ConsumerStatefulWidget {
|
||||
const StepScaffold({
|
||||
Key? key,
|
||||
required this.body,
|
||||
required this.step,
|
||||
required this.model,
|
||||
required this.initialStep,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget body;
|
||||
final int step;
|
||||
final IncompleteExchangeModel model;
|
||||
final int initialStep;
|
||||
|
||||
@override
|
||||
State<StepScaffold> createState() => _StepScaffoldState();
|
||||
ConsumerState<StepScaffold> createState() => _StepScaffoldState();
|
||||
}
|
||||
|
||||
class _StepScaffoldState extends State<StepScaffold> {
|
||||
int currentStep = 0;
|
||||
late final IncompleteExchangeModel model;
|
||||
class _StepScaffoldState extends ConsumerState<StepScaffold> {
|
||||
int currentStep = 1;
|
||||
bool enableNext = false;
|
||||
|
||||
late final Duration duration;
|
||||
|
||||
void updateEnableNext(bool enableNext) {
|
||||
if (enableNext != this.enableNext) {
|
||||
setState(() => this.enableNext = enableNext);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> createTrade() async {
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.overlay
|
||||
.withOpacity(0.6),
|
||||
child: const CustomLoadingOverlay(
|
||||
message: "Creating a trade",
|
||||
eventBus: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final ExchangeResponse<Trade> response = await ref
|
||||
.read(exchangeProvider)
|
||||
.createTrade(
|
||||
from: ref.read(desktopExchangeModelProvider)!.sendTicker,
|
||||
to: ref.read(desktopExchangeModelProvider)!.receiveTicker,
|
||||
fixedRate: ref.read(desktopExchangeModelProvider)!.rateType !=
|
||||
ExchangeRateType.estimated,
|
||||
amount: ref.read(desktopExchangeModelProvider)!.reversed
|
||||
? ref.read(desktopExchangeModelProvider)!.receiveAmount
|
||||
: ref.read(desktopExchangeModelProvider)!.sendAmount,
|
||||
addressTo: ref.read(desktopExchangeModelProvider)!.recipientAddress!,
|
||||
extraId: null,
|
||||
addressRefund: ref.read(desktopExchangeModelProvider)!.refundAddress!,
|
||||
refundExtraId: "",
|
||||
rateId: ref.read(desktopExchangeModelProvider)!.rateId,
|
||||
reversed: ref.read(desktopExchangeModelProvider)!.reversed,
|
||||
);
|
||||
|
||||
if (response.value == null) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) => SimpleDesktopDialog(
|
||||
title: "Failed to create trade",
|
||||
message: response.exception?.toString() ?? ""),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// save trade to hive
|
||||
await ref.read(tradesServiceProvider).add(
|
||||
trade: response.value!,
|
||||
shouldNotifyListeners: true,
|
||||
);
|
||||
|
||||
String status = response.value!.status;
|
||||
|
||||
ref.read(desktopExchangeModelProvider)!.trade = response.value!;
|
||||
|
||||
// extra info if status is waiting
|
||||
if (status == "Waiting") {
|
||||
status += " for deposit";
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
NotificationApi.showNotification(
|
||||
changeNowId: ref.read(desktopExchangeModelProvider)!.trade!.tradeId,
|
||||
title: status,
|
||||
body:
|
||||
"Trade ID ${ref.read(desktopExchangeModelProvider)!.trade!.tradeId}",
|
||||
walletId: "",
|
||||
iconAssetName: Assets.svg.arrowRotate,
|
||||
date: ref.read(desktopExchangeModelProvider)!.trade!.timestamp,
|
||||
shouldWatchForUpdates: true,
|
||||
coinName: "coinName",
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
// if (mounted) {
|
||||
// unawaited(
|
||||
// showDialog<void>(
|
||||
// context: context,
|
||||
// barrierColor: Colors.transparent,
|
||||
// barrierDismissible: false,
|
||||
// builder: (context) {
|
||||
// return DesktopDialog(
|
||||
// maxWidth: 720,
|
||||
// maxHeight: double.infinity,
|
||||
// child: StepScaffold(
|
||||
// initialStep: 4,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
void onBack() {
|
||||
if (currentStep > 1 && currentStep < 4) {
|
||||
setState(() => currentStep = currentStep - 1);
|
||||
} else if (currentStep == 1) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
currentStep = widget.step;
|
||||
model = widget.model;
|
||||
duration = const Duration(milliseconds: 250);
|
||||
currentStep = widget.initialStep;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final model = ref.watch(desktopExchangeModelProvider);
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -43,15 +194,16 @@ class _StepScaffoldState extends State<StepScaffold> {
|
|||
Row(
|
||||
children: [
|
||||
currentStep != 4
|
||||
? const AppBarBackButton(
|
||||
? AppBarBackButton(
|
||||
isCompact: true,
|
||||
iconSize: 23,
|
||||
onPressed: onBack,
|
||||
)
|
||||
: const SizedBox(
|
||||
width: 32,
|
||||
),
|
||||
Text(
|
||||
"Exchange ${model.sendTicker.toUpperCase()} to ${model.receiveTicker.toUpperCase()}",
|
||||
"Exchange ${model?.sendTicker.toUpperCase()} to ${model?.receiveTicker.toUpperCase()}",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
],
|
||||
|
@ -60,9 +212,6 @@ class _StepScaffoldState extends State<StepScaffold> {
|
|||
DesktopDialogCloseButton(
|
||||
onPressedOverride: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -85,7 +234,139 @@ class _StepScaffoldState extends State<StepScaffold> {
|
|||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: widget.body,
|
||||
child: FadeStack(
|
||||
index: currentStep - 1,
|
||||
children: [
|
||||
const DesktopStep1(),
|
||||
DesktopStep2(
|
||||
enableNextChanged: updateEnableNext,
|
||||
),
|
||||
const DesktopStep3(),
|
||||
const DesktopStep4(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 32,
|
||||
right: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
crossFadeState: currentStep == 4
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: SecondaryButton(
|
||||
label: "Back",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: onBack,
|
||||
),
|
||||
secondChild: SecondaryButton(
|
||||
label: "Send from Stack Wallet",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: onBack,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
crossFadeState: currentStep == 4
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
crossFadeState: currentStep == 3
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: PrimaryButton(
|
||||
label: "Next",
|
||||
enabled: currentStep != 2 ? true : enableNext,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () async {
|
||||
setState(() => currentStep = currentStep + 1);
|
||||
},
|
||||
),
|
||||
secondChild: PrimaryButton(
|
||||
label: "Confirm",
|
||||
enabled: currentStep != 2 ? true : enableNext,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () async {
|
||||
if (currentStep == 3) {
|
||||
final success = await createTrade();
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() => currentStep = currentStep + 1);
|
||||
},
|
||||
),
|
||||
),
|
||||
secondChild: PrimaryButton(
|
||||
label: "Show QR code",
|
||||
enabled: currentStep != 2 ? true : enableNext,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return DesktopDialog(
|
||||
maxHeight: 720,
|
||||
maxWidth: 720,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))} to this address",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Center(
|
||||
child: QrImage(
|
||||
// TODO: grab coin uri scheme from somewhere
|
||||
// data: "${coin.uriScheme}:$receivingAddress",
|
||||
data: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) =>
|
||||
value!.trade!.payInAddress)),
|
||||
size: 290,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Cancel",
|
||||
width: 310,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -1,26 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class DesktopStep1 extends ConsumerWidget {
|
||||
const DesktopStep1({
|
||||
Key? key,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
final IncompleteExchangeModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
|
@ -55,7 +47,7 @@ class DesktopStep1 extends ConsumerWidget {
|
|||
DesktopStepItem(
|
||||
label: "You send",
|
||||
value:
|
||||
"${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}",
|
||||
"${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))}",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -64,63 +56,20 @@ class DesktopStep1 extends ConsumerWidget {
|
|||
DesktopStepItem(
|
||||
label: "You receive",
|
||||
value:
|
||||
"~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}",
|
||||
"~${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))}",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
),
|
||||
DesktopStepItem(
|
||||
label: model.rateType == ExchangeRateType.estimated
|
||||
label: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.rateType)) ==
|
||||
ExchangeRateType.estimated
|
||||
? "Estimated rate"
|
||||
: "Fixed rate",
|
||||
value: model.rateInfo,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Back",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Next",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 720,
|
||||
maxHeight: double.infinity,
|
||||
child: StepScaffold(
|
||||
step: 2,
|
||||
model: model,
|
||||
body: DesktopStep2(
|
||||
model: model,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.rateInfo)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/models/contact_address_entry.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart';
|
||||
import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart';
|
||||
|
@ -18,31 +16,29 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
|||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class DesktopStep2 extends ConsumerStatefulWidget {
|
||||
const DesktopStep2({
|
||||
Key? key,
|
||||
required this.model,
|
||||
required this.enableNextChanged,
|
||||
this.clipboard = const ClipboardWrapper(),
|
||||
}) : super(key: key);
|
||||
|
||||
final IncompleteExchangeModel model;
|
||||
final ClipboardInterface clipboard;
|
||||
final void Function(bool) enableNextChanged;
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopStep2> createState() => _DesktopStep2State();
|
||||
}
|
||||
|
||||
class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
||||
late final IncompleteExchangeModel model;
|
||||
late final ClipboardInterface clipboard;
|
||||
|
||||
late final TextEditingController _toController;
|
||||
|
@ -51,8 +47,6 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
late final FocusNode _toFocusNode;
|
||||
late final FocusNode _refundFocusNode;
|
||||
|
||||
bool enableNext = false;
|
||||
|
||||
bool isStackCoin(String ticker) {
|
||||
try {
|
||||
coinFromTickerCaseInsensitive(ticker);
|
||||
|
@ -65,10 +59,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
void selectRecipientAddressFromStack() async {
|
||||
try {
|
||||
final coin = coinFromTickerCaseInsensitive(
|
||||
model.receiveTicker,
|
||||
ref.read(desktopExchangeModelProvider)!.receiveTicker,
|
||||
);
|
||||
|
||||
final address = await showDialog<String?>(
|
||||
final info = await showDialog<Tuple2<String, String>?>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => DesktopDialog(
|
||||
|
@ -83,29 +77,25 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
),
|
||||
);
|
||||
|
||||
if (address is String) {
|
||||
final manager =
|
||||
ref.read(walletsChangeNotifierProvider).getManager(address);
|
||||
|
||||
_toController.text = manager.walletName;
|
||||
model.recipientAddress = await manager.currentReceivingAddress;
|
||||
if (info is Tuple2<String, String>) {
|
||||
_toController.text = info.item1;
|
||||
ref.read(desktopExchangeModelProvider)!.recipientAddress = info.item2;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e\n$s", level: LogLevel.Info);
|
||||
}
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty;
|
||||
});
|
||||
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty);
|
||||
}
|
||||
|
||||
void selectRefundAddressFromStack() async {
|
||||
try {
|
||||
final coin = coinFromTickerCaseInsensitive(
|
||||
model.sendTicker,
|
||||
ref.read(desktopExchangeModelProvider)!.sendTicker,
|
||||
);
|
||||
|
||||
final address = await showDialog<String?>(
|
||||
final info = await showDialog<Tuple2<String, String>?>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => DesktopDialog(
|
||||
|
@ -119,25 +109,20 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
),
|
||||
),
|
||||
);
|
||||
if (address is String) {
|
||||
final manager =
|
||||
ref.read(walletsChangeNotifierProvider).getManager(address);
|
||||
|
||||
_refundController.text = manager.walletName;
|
||||
model.refundAddress = await manager.currentReceivingAddress;
|
||||
if (info is Tuple2<String, String>) {
|
||||
_refundController.text = info.item1;
|
||||
ref.read(desktopExchangeModelProvider)!.refundAddress = info.item2;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e\n$s", level: LogLevel.Info);
|
||||
}
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty;
|
||||
});
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty);
|
||||
}
|
||||
|
||||
void selectRecipientFromAddressBook() async {
|
||||
final coin = coinFromTickerCaseInsensitive(
|
||||
model.receiveTicker,
|
||||
ref.read(desktopExchangeModelProvider)!.receiveTicker,
|
||||
);
|
||||
|
||||
final entry = await showDialog<ContactAddressEntry?>(
|
||||
|
@ -176,17 +161,15 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
|
||||
if (entry != null) {
|
||||
_toController.text = entry.address;
|
||||
model.recipientAddress = entry.address;
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty;
|
||||
});
|
||||
ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
void selectRefundFromAddressBook() async {
|
||||
final coin = coinFromTickerCaseInsensitive(
|
||||
model.sendTicker,
|
||||
ref.read(desktopExchangeModelProvider)!.sendTicker,
|
||||
);
|
||||
|
||||
final entry = await showDialog<ContactAddressEntry?>(
|
||||
|
@ -225,17 +208,14 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
|
||||
if (entry != null) {
|
||||
_refundController.text = entry.address;
|
||||
model.refundAddress = entry.address;
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty;
|
||||
});
|
||||
ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
model = widget.model;
|
||||
clipboard = widget.clipboard;
|
||||
|
||||
_toController = TextEditingController();
|
||||
|
@ -246,7 +226,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
|
||||
final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state;
|
||||
if (tuple != null) {
|
||||
if (model.receiveTicker.toLowerCase() ==
|
||||
if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() ==
|
||||
tuple.item2.ticker.toLowerCase()) {
|
||||
ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
|
@ -254,10 +234,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.currentReceivingAddress
|
||||
.then((value) {
|
||||
_toController.text = value;
|
||||
model.recipientAddress = _toController.text;
|
||||
ref.read(desktopExchangeModelProvider)!.recipientAddress =
|
||||
_toController.text;
|
||||
});
|
||||
} else {
|
||||
if (model.sendTicker.toUpperCase() ==
|
||||
if (ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
|
||||
tuple.item2.ticker.toUpperCase()) {
|
||||
ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
|
@ -265,7 +246,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.currentReceivingAddress
|
||||
.then((value) {
|
||||
_refundController.text = value;
|
||||
model.refundAddress = _refundController.text;
|
||||
ref.read(desktopExchangeModelProvider)!.refundAddress =
|
||||
_refundController.text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -316,7 +298,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight),
|
||||
),
|
||||
if (isStackCoin(model.receiveTicker))
|
||||
if (isStackCoin(ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.receiveTicker))))
|
||||
BlueTextButton(
|
||||
text: "Choose from stack",
|
||||
onTap: selectRecipientAddressFromStack,
|
||||
|
@ -349,13 +332,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
focusNode: _toFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
enableNext = _toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
widget.enableNextChanged.call(_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter the ${model.receiveTicker.toUpperCase()} payout address",
|
||||
"Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address",
|
||||
_toFocusNode,
|
||||
context,
|
||||
desktopMed: true,
|
||||
|
@ -380,11 +361,12 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
"sendViewClearAddressFieldButtonKey"),
|
||||
onTap: () {
|
||||
_toController.text = "";
|
||||
model.recipientAddress = _toController.text;
|
||||
setState(() {
|
||||
enableNext = _toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.recipientAddress = _toController.text;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
|
@ -398,12 +380,12 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
data!.text!.isNotEmpty) {
|
||||
final content = data.text!.trim();
|
||||
_toController.text = content;
|
||||
model.recipientAddress = _toController.text;
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.recipientAddress = _toController.text;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
}
|
||||
},
|
||||
child: _toController.text.isEmpty
|
||||
|
@ -411,7 +393,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
: const XIcon(),
|
||||
),
|
||||
if (_toController.text.isEmpty &&
|
||||
isStackCoin(model.receiveTicker))
|
||||
isStackCoin(ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.receiveTicker))))
|
||||
TextFieldIconButton(
|
||||
key: const Key("sendViewAddressBookButtonKey"),
|
||||
onTap: selectRecipientFromAddressBook,
|
||||
|
@ -430,7 +413,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
RoundedWhiteContainer(
|
||||
borderColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Text(
|
||||
"This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.",
|
||||
"This is the wallet where your ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} will be sent to.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
),
|
||||
|
@ -447,7 +430,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight),
|
||||
),
|
||||
if (isStackCoin(model.sendTicker))
|
||||
if (isStackCoin(ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker))))
|
||||
BlueTextButton(
|
||||
text: "Choose from stack",
|
||||
onTap: selectRefundAddressFromStack,
|
||||
|
@ -479,13 +463,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
focusNode: _refundFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
enableNext = _toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
widget.enableNextChanged.call(_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${model.sendTicker.toUpperCase()} refund address",
|
||||
"Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address",
|
||||
_refundFocusNode,
|
||||
context,
|
||||
desktopMed: true,
|
||||
|
@ -510,12 +492,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
"sendViewClearAddressFieldButtonKey"),
|
||||
onTap: () {
|
||||
_refundController.text = "";
|
||||
model.refundAddress = _refundController.text;
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.refundAddress = _refundController.text;
|
||||
|
||||
setState(() {
|
||||
enableNext = _toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
|
@ -530,13 +513,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
final content = data.text!.trim();
|
||||
|
||||
_refundController.text = content;
|
||||
model.refundAddress = _refundController.text;
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.refundAddress = _refundController.text;
|
||||
|
||||
setState(() {
|
||||
enableNext =
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty;
|
||||
});
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty);
|
||||
}
|
||||
},
|
||||
child: _refundController.text.isEmpty
|
||||
|
@ -544,7 +527,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
: const XIcon(),
|
||||
),
|
||||
if (_refundController.text.isEmpty &&
|
||||
isStackCoin(model.sendTicker))
|
||||
isStackCoin(ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker))))
|
||||
TextFieldIconButton(
|
||||
key: const Key("sendViewAddressBookButtonKey"),
|
||||
onTap: selectRefundFromAddressBook,
|
||||
|
@ -567,53 +551,6 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Back",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Next",
|
||||
enabled: enableNext,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 720,
|
||||
maxHeight: double.infinity,
|
||||
child: StepScaffold(
|
||||
step: 3,
|
||||
model: model,
|
||||
body: DesktopStep3(
|
||||
model: model,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,157 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart';
|
||||
import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart';
|
||||
import 'package:stackwallet/providers/exchange/exchange_provider.dart';
|
||||
import 'package:stackwallet/providers/global/trades_service_provider.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_response.dart';
|
||||
import 'package:stackwallet/services/notifications_api.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class DesktopStep3 extends ConsumerStatefulWidget {
|
||||
const DesktopStep3({
|
||||
Key? key,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
final IncompleteExchangeModel model;
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopStep3> createState() => _DesktopStep3State();
|
||||
}
|
||||
|
||||
class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
||||
late final IncompleteExchangeModel model;
|
||||
|
||||
Future<void> createTrade() async {
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.overlay
|
||||
.withOpacity(0.6),
|
||||
child: const CustomLoadingOverlay(
|
||||
message: "Creating a trade",
|
||||
eventBus: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final ExchangeResponse<Trade> response =
|
||||
await ref.read(exchangeProvider).createTrade(
|
||||
from: model.sendTicker,
|
||||
to: model.receiveTicker,
|
||||
fixedRate: model.rateType != ExchangeRateType.estimated,
|
||||
amount: model.reversed ? model.receiveAmount : model.sendAmount,
|
||||
addressTo: model.recipientAddress!,
|
||||
extraId: null,
|
||||
addressRefund: model.refundAddress!,
|
||||
refundExtraId: "",
|
||||
rateId: model.rateId,
|
||||
reversed: model.reversed,
|
||||
);
|
||||
|
||||
if (response.value == null) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) => SimpleDesktopDialog(
|
||||
title: "Failed to create trade",
|
||||
message: response.exception?.toString() ?? ""),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// save trade to hive
|
||||
await ref.read(tradesServiceProvider).add(
|
||||
trade: response.value!,
|
||||
shouldNotifyListeners: true,
|
||||
);
|
||||
|
||||
String status = response.value!.status;
|
||||
|
||||
model.trade = response.value!;
|
||||
|
||||
// extra info if status is waiting
|
||||
if (status == "Waiting") {
|
||||
status += " for deposit";
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
NotificationApi.showNotification(
|
||||
changeNowId: model.trade!.tradeId,
|
||||
title: status,
|
||||
body: "Trade ID ${model.trade!.tradeId}",
|
||||
walletId: "",
|
||||
iconAssetName: Assets.svg.arrowRotate,
|
||||
date: model.trade!.timestamp,
|
||||
shouldWatchForUpdates: true,
|
||||
coinName: "coinName",
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 720,
|
||||
maxHeight: double.infinity,
|
||||
child: StepScaffold(
|
||||
step: 4,
|
||||
model: model,
|
||||
body: DesktopStep4(
|
||||
model: model,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
model = widget.model;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
@ -179,7 +45,7 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
|||
DesktopStepItem(
|
||||
label: "You send",
|
||||
value:
|
||||
"${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}",
|
||||
"${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))}",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -188,17 +54,20 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
|||
DesktopStepItem(
|
||||
label: "You receive",
|
||||
value:
|
||||
"~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}",
|
||||
"~${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))}",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
),
|
||||
DesktopStepItem(
|
||||
label: model.rateType == ExchangeRateType.estimated
|
||||
label: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.rateType)) ==
|
||||
ExchangeRateType.estimated
|
||||
? "Estimated rate"
|
||||
: "Fixed rate",
|
||||
value: model.rateInfo,
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.rateInfo)),
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -206,8 +75,11 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
|||
),
|
||||
DesktopStepItem(
|
||||
vertical: true,
|
||||
label: "Recipient ${model.receiveTicker.toUpperCase()} address",
|
||||
value: model.recipientAddress!,
|
||||
label:
|
||||
"Recipient ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} address",
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.recipientAddress)) ??
|
||||
"Error",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -215,35 +87,11 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
|||
),
|
||||
DesktopStepItem(
|
||||
vertical: true,
|
||||
label: "Refund ${model.sendTicker.toUpperCase()} address",
|
||||
value: model.refundAddress!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Back",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Confirm",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: createTrade,
|
||||
),
|
||||
label:
|
||||
"Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address",
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.refundAddress)) ??
|
||||
"Error",
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,38 +1,26 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
||||
import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/route_generator.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/rounded_container.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class DesktopStep4 extends ConsumerStatefulWidget {
|
||||
const DesktopStep4({
|
||||
Key? key,
|
||||
required this.model,
|
||||
}) : super(key: key);
|
||||
|
||||
final IncompleteExchangeModel model;
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopStep4> createState() => _DesktopStep4State();
|
||||
}
|
||||
|
||||
class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
||||
late final IncompleteExchangeModel model;
|
||||
|
||||
String _statusString = "New";
|
||||
|
||||
Timer? _statusTimer;
|
||||
|
@ -51,8 +39,13 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
}
|
||||
|
||||
Future<void> _updateStatus() async {
|
||||
final statusResponse =
|
||||
await ref.read(exchangeProvider).updateTrade(model.trade!);
|
||||
final trade = ref.read(desktopExchangeModelProvider)?.trade;
|
||||
|
||||
if (trade == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final statusResponse = await ref.read(exchangeProvider).updateTrade(trade);
|
||||
String status = "Waiting";
|
||||
if (statusResponse.value != null) {
|
||||
status = statusResponse.value!.status;
|
||||
|
@ -72,8 +65,6 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
model = widget.model;
|
||||
|
||||
_statusTimer = Timer.periodic(const Duration(seconds: 60), (_) {
|
||||
_updateStatus();
|
||||
});
|
||||
|
@ -93,14 +84,14 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
"Send ${model.sendTicker.toUpperCase()} to the address below",
|
||||
"Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Text(
|
||||
"Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.",
|
||||
"Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below. Once it is received, ${ref.watch(desktopExchangeModelProvider.select((value) => value!.trade?.exchangeName))} will send the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} to the recipient address you provided. You can find this trade details and check its status in the list of trades.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -111,7 +102,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
child: RichText(
|
||||
text: TextSpan(
|
||||
text:
|
||||
"You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ",
|
||||
"You must send at least ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}. ",
|
||||
style: STextStyles.label700(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -121,7 +112,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.",
|
||||
"If you send less than ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}, your transaction may not be converted and it may not be refunded.",
|
||||
style: STextStyles.label(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -143,8 +134,11 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
children: [
|
||||
DesktopStepItem(
|
||||
vertical: true,
|
||||
label: "Send ${model.sendTicker.toUpperCase()} to this address",
|
||||
value: model.trade!.payInAddress,
|
||||
label:
|
||||
"Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to this address",
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.trade?.payInAddress)) ??
|
||||
"Error",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -153,7 +147,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
DesktopStepItem(
|
||||
label: "Amount",
|
||||
value:
|
||||
"${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}",
|
||||
"${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))}",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -161,7 +155,9 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
),
|
||||
DesktopStepItem(
|
||||
label: "Trade ID",
|
||||
value: model.trade!.tradeId,
|
||||
value: ref.watch(desktopExchangeModelProvider
|
||||
.select((value) => value!.trade?.tradeId)) ??
|
||||
"Error",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
|
@ -191,110 +187,6 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Send from Stack Wallet",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
final trade = model.trade!;
|
||||
final amount = Decimal.parse(trade.payInAmount);
|
||||
final address = trade.payInAddress;
|
||||
|
||||
final coin =
|
||||
coinFromTickerCaseInsensitive(trade.payInCurrency);
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => Navigator(
|
||||
initialRoute: SendFromView.routeName,
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
onGenerateInitialRoutes: (_, __) {
|
||||
return [
|
||||
FadePageRoute(
|
||||
SendFromView(
|
||||
coin: coin,
|
||||
trade: trade,
|
||||
amount: amount,
|
||||
address: address,
|
||||
shouldPopRoot: true,
|
||||
fromDesktopStep4: true,
|
||||
),
|
||||
const RouteSettings(
|
||||
name: SendFromView.routeName,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Show QR code",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return DesktopDialog(
|
||||
maxHeight: 720,
|
||||
maxWidth: 720,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Send ${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker} to this address",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Center(
|
||||
child: QrImage(
|
||||
// TODO: grab coin uri scheme from somewhere
|
||||
// data: "${coin.uriScheme}:$receivingAddress",
|
||||
data: model.trade!.payInAddress,
|
||||
size: 290,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Cancel",
|
||||
width: 310,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
|
|||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class DesktopChooseFromStack extends ConsumerStatefulWidget {
|
||||
const DesktopChooseFromStack({
|
||||
|
@ -222,8 +223,18 @@ class _DesktopChooseFromStackState
|
|||
),
|
||||
BlueTextButton(
|
||||
text: "Select wallet",
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(manager.walletId);
|
||||
onTap: () async {
|
||||
final address =
|
||||
await manager.currentReceivingAddress;
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(
|
||||
Tuple2(
|
||||
manager.walletName,
|
||||
address,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -22,41 +22,54 @@ class SimpleDesktopDialog extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
message,
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
message,
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
),
|
||||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Ok",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32,
|
||||
right: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Ok",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
57
lib/widgets/fade_stack.dart
Normal file
57
lib/widgets/fade_stack.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FadeStack extends StatefulWidget {
|
||||
final int index;
|
||||
final List<Widget> children;
|
||||
|
||||
const FadeStack({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
FadeStackState createState() => FadeStackState();
|
||||
}
|
||||
|
||||
class FadeStackState extends State<FadeStack>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController animationController;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FadeStack oldWidget) {
|
||||
if (widget.index != oldWidget.index) {
|
||||
animationController.forward(from: 0.0);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
);
|
||||
animationController.forward();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animationController,
|
||||
child: IndexedStack(
|
||||
index: widget.index,
|
||||
children: widget.children,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue