From 86ca402401d231bdca3d59c32a60d26a05a73b61 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 Feb 2023 07:31:31 -0600 Subject: [PATCH] close exchange step 4 back to wallet or exchange home view --- lib/models/exchange/incomplete_exchange.dart | 2 + lib/pages/exchange_view/exchange_form.dart | 1 + .../exchange_step_views/step_4_view.dart | 1073 +++++++++-------- 3 files changed, 561 insertions(+), 515 deletions(-) diff --git a/lib/models/exchange/incomplete_exchange.dart b/lib/models/exchange/incomplete_exchange.dart index f24f52bc1..864a25490 100644 --- a/lib/models/exchange/incomplete_exchange.dart +++ b/lib/models/exchange/incomplete_exchange.dart @@ -15,6 +15,7 @@ class IncompleteExchangeModel extends ChangeNotifier { final ExchangeRateType rateType; final bool reversed; + final bool walletInitiated; String? _recipientAddress; @@ -68,6 +69,7 @@ class IncompleteExchangeModel extends ChangeNotifier { required this.receiveAmount, required this.rateType, required this.reversed, + required this.walletInitiated, String? rateId, }) : _rateId = rateId; } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 98ae53171..ba40f4d0f 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -558,6 +558,7 @@ class _ExchangeFormState extends ConsumerState { rateType: rateType, rateId: estimate.rateId, reversed: estimate.reversed, + walletInitiated: walletInitiated, ); if (mounted) { diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 1278d1ddb..2507ac2d4 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -105,551 +106,593 @@ class _Step4ViewState extends ConsumerState { super.dispose(); } + Future _close() async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName( + model.walletInitiated ? WalletView.routeName : HomeView.routeName, + ), + ); + } + } + @override Widget build(BuildContext context) { final bool isWalletCoin = _isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref); - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return WillPopScope( + onWillPop: () async { + await _close(); + return false; + }, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: Padding( + padding: const EdgeInsets.all(10), + child: AppBarIconButton( + size: 32, + color: Theme.of(context).extension()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.x, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: _close, + ), + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 3, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below", - style: STextStyles.pageTitleH1(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.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 3, + width: width, + ), + const SizedBox( + height: 14, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below", + style: STextStyles.pageTitleH1(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.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + RoundedContainer( + color: Theme.of(context) + .extension()! + .warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension()! + .warningForeground, + ), + 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.", + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .warningForeground, + ), + ), + ], ), + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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.", - style: STextStyles.label(context).copyWith( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: + STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: + model.sendAmount.toString()); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Send ${model.sendTicker.toUpperCase()} to this address", + style: + STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.payInAddress); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + model.trade!.payInAddress, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Trade ID", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + Text( + model.trade!.tradeId, + style: + STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.tradeId); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension()! + .infoItemIcons, + width: 12, + ), + ) + ], + ) + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + Text( + _statusString, + style: STextStyles.itemSubtitle(context) + .copyWith( color: Theme.of(context) .extension()! - .warningForeground, + .colorForStatus(_statusString), ), ), ], ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.sendAmount.toString()); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${model.sendTicker.toUpperCase()} to this address", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.payInAddress); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - model.trade!.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Trade ID", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - Text( - model.trade!.tradeId, - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.tradeId); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, - width: 12, - ), - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - Text( - _statusString, - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension()! - .colorForStatus(_statusString), - ), - ), - ], - ), - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (_) { - return StackDialogBase( - child: Column( - children: [ - const SizedBox( - height: 8, - ), - Center( - child: Text( - "Send ${model.sendTicker} to this address", - style: STextStyles.pageTitleH2( - context), - ), - ), - const SizedBox( - height: 24, - ), - Center( - child: QrImage( - // TODO: grab coin uri scheme from somewhere - // data: "${coin.uriScheme}:$receivingAddress", - data: model.trade!.payInAddress, - size: MediaQuery.of(context) - .size - .width / - 2, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - const SizedBox( - height: 24, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: TextButton( - onPressed: () => - Navigator.of(context).pop(), - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle( - context), - child: Text( - "Cancel", - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ) - ], - ), - ); - }, - ); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), - ), - if (isWalletCoin) + const Spacer(), const SizedBox( height: 12, ), - if (isWalletCoin) - Builder( - builder: (context) { - String buttonTitle = "Send from Stack Wallet"; - - final tuple = ref - .read(exchangeSendFromWalletIdStateProvider - .state) - .state; - if (tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase()) { - final walletName = ref - .read(walletsChangeNotifierProvider) - .getManager(tuple.item1) - .walletName; - buttonTitle = "Send from $walletName"; - } - - return TextButton( - onPressed: tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase() - ? () async { - final manager = ref - .read( - walletsChangeNotifierProvider) - .getManager(tuple.item1); - - final amount = - Format.decimalAmountToSatoshis( - model.sendAmount, - manager.coin); - final address = - model.trade!.payInAddress; - - try { - bool wasCancelled = false; - - unawaited(showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = - await manager.prepareSend( - address: address, - satoshiAmount: amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); - } - - txData["note"] = - "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => - ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: tuple.item1, - routeOnSuccessName: - HomeView.routeName, - trade: model.trade!, - ), - settings: - const RouteSettings( - name: - ConfirmChangeNowSendView - .routeName, + TextButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + const SizedBox( + height: 8, + ), + Center( + child: Text( + "Send ${model.sendTicker} to this address", + style: STextStyles.pageTitleH2( + context), + ), + ), + const SizedBox( + height: 24, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: MediaQuery.of(context) + .size + .width / + 2, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: TextButton( + onPressed: () => + Navigator.of(context) + .pop(), + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context), + child: Text( + "Cancel", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .buttonTextSecondary, ), ), - )); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); + ), + ), + ], + ) + ], + ), + ); + }, + ); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), + ), + if (isWalletCoin) + const SizedBox( + height: 12, + ), + if (isWalletCoin) + Builder( + builder: (context) { + String buttonTitle = "Send from Stack Wallet"; + + final tuple = ref + .read( + exchangeSendFromWalletIdStateProvider + .state) + .state; + if (tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + final walletName = ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .walletName; + buttonTitle = "Send from $walletName"; + } + + return TextButton( + onPressed: tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase() + ? () async { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(tuple.item1); + + final amount = + Format.decimalAmountToSatoshis( + model.sendAmount, + manager.coin); + final address = + model.trade!.payInAddress; + + try { + bool wasCancelled = false; + + unawaited(showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; - unawaited(showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension< - StackColors>()! - .getSecondaryEnabledButtonStyle( - context), - child: Text( - "Ok", - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - onPressed: () { Navigator.of(context) .pop(); }, - ), - ); - }, - )); - // } + ); + }, + )); + + final txData = + await manager.prepareSend( + address: address, + satoshiAmount: amount, + args: { + "feeRate": + FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of(context).pop(); + } + + txData["note"] = + "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + unawaited( + Navigator.of(context) + .push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => + ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: tuple.item1, + routeOnSuccessName: + HomeView.routeName, + trade: model.trade!, + ), + settings: + const RouteSettings( + name: + ConfirmChangeNowSendView + .routeName, + ), + ), + )); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension< + StackColors>()! + .getSecondaryEnabledButtonStyle( + context), + child: Text( + "Ok", + style: + STextStyles.button( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context) + .pop(); + }, + ), + ); + }, + )); + // } + } } - } - : () { - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (BuildContext context) { - return SendFromView( - coin: - coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency), - amount: model.sendAmount, - address: - model.trade!.payInAddress, - trade: model.trade!, - ); - }, - settings: const RouteSettings( - name: SendFromView.routeName, + : () { + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: + (BuildContext context) { + return SendFromView( + coin: + coinFromTickerCaseInsensitive( + model.trade! + .payInCurrency), + amount: model.sendAmount, + address: model + .trade!.payInAddress, + trade: model.trade!, + ); + }, + settings: const RouteSettings( + name: SendFromView.routeName, + ), ), - ), - ); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - buttonTitle, - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + ); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context), + child: Text( + buttonTitle, + style: + STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), );