From fa2e9cd8107c645a3bb4a50491b8a8b6ec6152db Mon Sep 17 00:00:00 2001 From: Serhii-Borodenko Date: Thu, 7 Jul 2022 18:02:38 +0300 Subject: [PATCH 01/87] fix exchangenow payment request --- lib/src/screens/exchange/widgets/exchange_card.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 384fc8d23..a330675fd 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -316,9 +316,10 @@ class ExchangeCardState extends State { if (amountController.text.isNotEmpty) { _showAmountPopup(context, paymentRequest); - } else { - amountController.text = paymentRequest.amount; + return; } + widget.amountFocusNode.requestFocus(); + amountController.text = paymentRequest.amount; }, placeholder: widget.hasRefundAddress ? S.of(context).refund_address @@ -465,6 +466,7 @@ class ExchangeCardState extends State { rightButtonText: S.of(context).ok, leftButtonText: S.of(context).cancel, actionRightButton: () { + widget.amountFocusNode.requestFocus(); amountController.text = paymentRequest.amount; Navigator.of(context).pop(); }, From 52b8bafac9baa98c874f1b64c74963918564885c Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 11 Jul 2022 18:20:16 +0300 Subject: [PATCH 02/87] Exchange screens access improvements (#409) * move tag prop to the crypto currency * add currency icons --- assets/images/zaddr_icon.png | Bin 0 -> 16312 bytes assets/images/zec_icon.png | Bin 0 -> 16312 bytes cw_core/lib/crypto_currency.dart | 81 +++++--- .../changenow_exchange_provider.dart | 26 +-- .../sideshift_exchange_provider.dart | 11 +- .../exchange/widgets/currency_picker.dart | 49 ++--- .../widgets/currency_picker_widget.dart | 2 +- .../exchange/widgets/currency_utils.dart | 101 --------- .../exchange/widgets/exchange_card.dart | 36 +++- lib/src/screens/send/widgets/send_card.dart | 196 +++++++++++------- 10 files changed, 237 insertions(+), 265 deletions(-) create mode 100644 assets/images/zaddr_icon.png create mode 100644 assets/images/zec_icon.png delete mode 100644 lib/src/screens/exchange/widgets/currency_utils.dart diff --git a/assets/images/zaddr_icon.png b/assets/images/zaddr_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..139acf84c54f2e3622db5346db88043071dea2c2 GIT binary patch literal 16312 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 Zso}Sq{Fyg?a{MECqG6z3{@5<;e*gqh6AJ(U literal 0 HcmV?d00001 diff --git a/assets/images/zec_icon.png b/assets/images/zec_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..139acf84c54f2e3622db5346db88043071dea2c2 GIT binary patch literal 16312 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 Zso}Sq{Fyg?a{MECqG6z3{@5<;e*gqh6AJ(U literal 0 HcmV?d00001 diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index ddcdb5856..802ef5e3e 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -5,9 +5,13 @@ part 'crypto_currency.g.dart'; @HiveType(typeId: 0) class CryptoCurrency extends EnumerableItem with Serializable { - const CryptoCurrency({final String title, final int raw}) + const CryptoCurrency({final String title, this.tag, this.name, this.iconPath, final int raw}) : super(title: title, raw: raw); + final String tag; + final String name; + final String iconPath; + static const all = [ CryptoCurrency.xmr, CryptoCurrency.ada, @@ -24,39 +28,44 @@ class CryptoCurrency extends EnumerableItem with Serializable { CryptoCurrency.usdterc20, CryptoCurrency.xlm, CryptoCurrency.xrp, - CryptoCurrency.xhv + CryptoCurrency.xhv, + CryptoCurrency.zaddr, + CryptoCurrency.zec ]; - static const xmr = CryptoCurrency(title: 'XMR', raw: 0); - static const ada = CryptoCurrency(title: 'ADA', raw: 1); - static const bch = CryptoCurrency(title: 'BCH', raw: 2); - static const bnb = CryptoCurrency(title: 'BNB BEP2', raw: 3); - static const btc = CryptoCurrency(title: 'BTC', raw: 4); - static const dai = CryptoCurrency(title: 'DAI', raw: 5); - static const dash = CryptoCurrency(title: 'DASH', raw: 6); - static const eos = CryptoCurrency(title: 'EOS', raw: 7); - static const eth = CryptoCurrency(title: 'ETH', raw: 8); - static const ltc = CryptoCurrency(title: 'LTC', raw: 9); + static const xmr = CryptoCurrency(title: 'XMR', iconPath: 'assets/images/monero_icon.png', name: 'Monero', raw: 0); + static const ada = CryptoCurrency(title: 'ADA', iconPath: 'assets/images/ada_icon.png', name: 'Cardano', raw: 1); + static const bch = CryptoCurrency(title: 'BCH', iconPath: 'assets/images/bch_icon.png',name: 'Bitcoin Cash', raw: 2); + static const bnb = CryptoCurrency(title: 'BNB', iconPath: 'assets/images/bnb_icon.png', tag: 'BSC', name: 'Binance Coin', raw: 3); + static const btc = CryptoCurrency(title: 'BTC', iconPath: 'assets/images/btc.png', name: 'Bitcoin', raw: 4); + static const dai = CryptoCurrency(title: 'DAI', iconPath: 'assets/images/dai_icon.png', tag: 'ETH', name: 'Dai', raw: 5); + static const dash = CryptoCurrency(title: 'DASH', iconPath: 'assets/images/dash_icon.png', name: 'Dash', raw: 6); + static const eos = CryptoCurrency(title: 'EOS', iconPath: 'assets/images/eos_icon.png', name: 'EOS', raw: 7); + static const eth = CryptoCurrency(title: 'ETH', iconPath: 'assets/images/eth_icon.png', name: 'Ethereum', raw: 8); + static const ltc = CryptoCurrency(title: 'LTC', iconPath: 'assets/images/litecoin-ltc_icon.png', name: 'Litecoin',raw: 9); static const nano = CryptoCurrency(title: 'NANO', raw: 10); - static const trx = CryptoCurrency(title: 'TRX', raw: 11); - static const usdt = CryptoCurrency(title: 'USDT', raw: 12); - static const usdterc20 = CryptoCurrency(title: 'USDTERC20', raw: 13); - static const xlm = CryptoCurrency(title: 'XLM', raw: 14); - static const xrp = CryptoCurrency(title: 'XRP', raw: 15); - static const xhv = CryptoCurrency(title: 'XHV', raw: 16); - - static const xag = CryptoCurrency(title: 'XAG', raw: 17); - static const xau = CryptoCurrency(title: 'XAU', raw: 18); - static const xaud = CryptoCurrency(title: 'XAUD', raw: 19); - static const xbtc = CryptoCurrency(title: 'XBTC', raw: 20); - static const xcad = CryptoCurrency(title: 'XCAD', raw: 21); - static const xchf = CryptoCurrency(title: 'XCHF', raw: 22); - static const xcny = CryptoCurrency(title: 'XCNY', raw: 23); - static const xeur = CryptoCurrency(title: 'XEUR', raw: 24); - static const xgbp = CryptoCurrency(title: 'XGBP', raw: 25); - static const xjpy = CryptoCurrency(title: 'XJPY', raw: 26); - static const xnok = CryptoCurrency(title: 'XNOK', raw: 27); - static const xnzd = CryptoCurrency(title: 'XNZD', raw: 28); - static const xusd = CryptoCurrency(title: 'XUSD', raw: 29); + static const trx = CryptoCurrency(title: 'TRX', iconPath: 'assets/images/trx_icon.png', name: 'TRON', raw: 11); + static const usdt = CryptoCurrency(title: 'USDT', iconPath: 'assets/images/usdt_icon.png', tag: 'OMNI', name: 'USDT', raw: 12); + static const usdterc20 = CryptoCurrency(title: 'USDT', iconPath: 'assets/images/usdterc20_icon.png', tag: 'ETH', name: 'USDT', raw: 13); + static const xlm = CryptoCurrency(title: 'XLM', iconPath: 'assets/images/xlm_icon.png', name: 'Stellar', raw: 14); + static const xrp = CryptoCurrency(title: 'XRP', iconPath: 'assets/images/xrp_icon.png', name: 'Ripple', raw: 15); + static const xhv = CryptoCurrency(title: 'XHV', iconPath: 'assets/images/xhv_logo.png', name: 'Haven Protocol', raw: 16); + + static const xag = CryptoCurrency(title: 'XAG', tag: 'XHV', raw: 17); + static const xau = CryptoCurrency(title: 'XAU', tag: 'XHV', raw: 18); + static const xaud = CryptoCurrency(title: 'XAUD', tag: 'XHV', raw: 19); + static const xbtc = CryptoCurrency(title: 'XBTC', tag: 'XHV', raw: 20); + static const xcad = CryptoCurrency(title: 'XCAD', tag: 'XHV', raw: 21); + static const xchf = CryptoCurrency(title: 'XCHF', tag: 'XHV', raw: 22); + static const xcny = CryptoCurrency(title: 'XCNY', tag: 'XHV', raw: 23); + static const xeur = CryptoCurrency(title: 'XEUR', tag: 'XHV', raw: 24); + static const xgbp = CryptoCurrency(title: 'XGBP', tag: 'XHV', raw: 25); + static const xjpy = CryptoCurrency(title: 'XJPY', tag: 'XHV', raw: 26); + static const xnok = CryptoCurrency(title: 'XNOK', tag: 'XHV', raw: 27); + static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28); + static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29); + + static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', name: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 30); + static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', name: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 31); static CryptoCurrency deserialize({int raw}) { switch (raw) { @@ -120,6 +129,10 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.xnzd; case 29: return CryptoCurrency.xusd; + case 30: + return CryptoCurrency.zaddr; + case 31: + return CryptoCurrency.zec; default: return null; } @@ -187,6 +200,10 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.xnzd; case 'xusd': return CryptoCurrency.xusd; + case 'zaddr': + return CryptoCurrency.zaddr; + case 'zec': + return CryptoCurrency.zec; default: return null; } diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index ba5c18bdc..c6594516d 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -240,33 +240,23 @@ class ChangeNowExchangeProvider extends ExchangeProvider { } String networkFor(CryptoCurrency currency) { - const bnbTitle = 'bnb'; - switch (currency) { case CryptoCurrency.usdt: return CryptoCurrency.btc.title.toLowerCase(); - case CryptoCurrency.usdterc20: - return CryptoCurrency.eth.title.toLowerCase(); - case CryptoCurrency.bnb: - return bnbTitle; - case CryptoCurrency.dai: - return CryptoCurrency.eth.title.toLowerCase(); default: - return currency.title.toLowerCase(); + return currency.tag != null + ? currency.tag.toLowerCase() + : currency.title.toLowerCase(); + } } } - static String normalizeCryptoCurrency(CryptoCurrency currency) { - const bnbTitle = 'bnb'; - - switch(currency) { - case CryptoCurrency.bnb: - return bnbTitle; - case CryptoCurrency.usdterc20: - return CryptoCurrency.usdt.title.toLowerCase(); + String normalizeCryptoCurrency(CryptoCurrency currency) { + switch(currency) { + case CryptoCurrency.zec: + return 'zec'; default: return currency.title.toLowerCase(); } } -} diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index d4db3dcae..b828c2f6f 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -250,14 +250,15 @@ class SideShiftExchangeProvider extends ExchangeProvider { String get title => 'SideShift'; static String normalizeCryptoCurrency(CryptoCurrency currency) { - const bnbTitle = 'bsc'; - const usdterc20 = 'usdtErc20'; - switch (currency) { + case CryptoCurrency.zaddr: + return 'zaddr'; + case CryptoCurrency.zec: + return 'zec'; case CryptoCurrency.bnb: - return bnbTitle; + return currency.tag.toLowerCase(); case CryptoCurrency.usdterc20: - return usdterc20; + return 'usdtErc20'; default: return currency.title.toLowerCase(); } diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 5e4a31831..abe07e56b 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -1,6 +1,5 @@ import 'dart:ui'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker_item_widget.dart'; -import 'package:cake_wallet/src/screens/exchange/widgets/currency_utils.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/picker_item.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:flutter/cupertino.dart'; @@ -35,45 +34,33 @@ class CurrencyPickerState extends State { CurrencyPickerState(this.items) : isSearchBarActive = false, textFieldValue = '', - subPickerItemsList = [], + subPickerItemsList = items, appBarTextStyle = - TextStyle(fontSize: 20, fontFamily: 'Lato', backgroundColor: Colors.transparent, color: Colors.white); + TextStyle(fontSize: 20, fontFamily: 'Lato', backgroundColor: Colors.transparent, color: Colors.white); + - @override - void initState() { - pickerItemsList = CryptoCurrency.all - .map((CryptoCurrency cur) => PickerItem(cur, - title: CurrencyUtils.titleForCurrency(cur), - iconPath: CurrencyUtils.iconPathForCurrency(cur), - tag: CurrencyUtils.tagForCurrency(cur), - description: CurrencyUtils.descriptionForCurrency(cur))) - .toList(); - cleanSubPickerItemsList(); - super.initState(); - } List> pickerItemsList; List items; bool isSearchBarActive; String textFieldValue; - List> subPickerItemsList; + List subPickerItemsList; TextStyle appBarTextStyle; - void cleanSubPickerItemsList() { - subPickerItemsList = pickerItemsList.where((element) => items.contains(element.original)).toList(); - } + void cleanSubPickerItemsList() => subPickerItemsList = items; - void currencySearchBySubstring(String subString, List> list) { + void currencySearchBySubstring(String subString) { setState(() { if (subString.isNotEmpty) { - subPickerItemsList = subPickerItemsList + subPickerItemsList = items .where((element) => - element.title.contains(subString.toUpperCase()) || - element.description.contains(subString.toLowerCase())) + (element.title != null ? element.title.toLowerCase().contains(subString.toLowerCase()) : false) || + (element.tag != null ? element.tag.toLowerCase().contains(subString.toLowerCase()) : false) || + (element.name != null ? element.name.toLowerCase().contains(subString.toLowerCase()) : false)) .toList(); - } else { - cleanSubPickerItemsList(); + return; } + cleanSubPickerItemsList(); }); } @@ -140,7 +127,7 @@ class CurrencyPickerState extends State { onChanged: (value) { this.textFieldValue = value; cleanSubPickerItemsList(); - currencySearchBySubstring(textFieldValue, subPickerItemsList); + currencySearchBySubstring(textFieldValue); }, ), ), @@ -152,10 +139,10 @@ class CurrencyPickerState extends State { AspectRatio( aspectRatio: 6, child: PickerItemWidget( - title: pickerItemsList[widget.selectedAtIndex].title, - iconPath: pickerItemsList[widget.selectedAtIndex].iconPath, + title: items[widget.selectedAtIndex].title, + iconPath: items[widget.selectedAtIndex].iconPath, isSelected: true, - tag: pickerItemsList[widget.selectedAtIndex].tag, + tag: items[widget.selectedAtIndex].tag, ), ), Flexible( @@ -167,10 +154,10 @@ class CurrencyPickerState extends State { setState(() { widget.selectedAtIndex = index; }); - widget.onItemSelected(subPickerItemsList[index].original); + widget.onItemSelected(subPickerItemsList[index]); if (widget.isConvertFrom && !widget.isMoneroWallet && - (subPickerItemsList[index].original == CryptoCurrency.xmr)) { + (subPickerItemsList[index] == CryptoCurrency.xmr)) { } else { Navigator.of(context).pop(); } diff --git a/lib/src/screens/exchange/widgets/currency_picker_widget.dart b/lib/src/screens/exchange/widgets/currency_picker_widget.dart index ec0a11356..254e85d4a 100644 --- a/lib/src/screens/exchange/widgets/currency_picker_widget.dart +++ b/lib/src/screens/exchange/widgets/currency_picker_widget.dart @@ -14,7 +14,7 @@ class CurrencyPickerWidget extends StatelessWidget { final int crossAxisCount; final int selectedAtIndex; final Function pickListItem; - final List> pickerItemsList; + final List pickerItemsList; final ScrollController _scrollController = ScrollController(); diff --git a/lib/src/screens/exchange/widgets/currency_utils.dart b/lib/src/screens/exchange/widgets/currency_utils.dart deleted file mode 100644 index 83c6d0af9..000000000 --- a/lib/src/screens/exchange/widgets/currency_utils.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; - -class CurrencyUtils { - static String tagForCurrency(CryptoCurrency cur) { - switch (cur) { - case CryptoCurrency.bnb: - return 'BEP2'; - case CryptoCurrency.dai: - return 'ETH'; - case CryptoCurrency.usdt: - return 'OMNI'; - case CryptoCurrency.usdterc20: - return 'ETH'; - default: - return null; - } - } - - static String iconPathForCurrency(CryptoCurrency cur) { - switch (cur) { - case CryptoCurrency.xmr: - return 'assets/images/monero_icon.png'; - case CryptoCurrency.ada: - return 'assets/images/ada_icon.png'; - case CryptoCurrency.bch: - return 'assets/images/bch_icon.png'; - case CryptoCurrency.bnb: - return 'assets/images/bnb_icon.png'; - case CryptoCurrency.btc: - return 'assets/images/btc.png'; - case CryptoCurrency.dai: - return 'assets/images/dai_icon.png'; - case CryptoCurrency.dash: - return 'assets/images/dash_icon.png'; - case CryptoCurrency.eos: - return 'assets/images/eos_icon.png'; - case CryptoCurrency.eth: - return 'assets/images/eth_icon.png'; - case CryptoCurrency.ltc: - return 'assets/images/litecoin-ltc_icon.png'; - case CryptoCurrency.trx: - return 'assets/images/trx_icon.png'; - case CryptoCurrency.usdt: - return 'assets/images/usdt_icon.png'; - case CryptoCurrency.usdterc20: - return 'assets/images/usdterc20_icon.png'; - case CryptoCurrency.xlm: - return 'assets/images/xlm_icon.png'; - case CryptoCurrency.xrp: - return 'assets/images/xrp_icon.png'; - case CryptoCurrency.xhv: - return 'assets/images/xhv_logo.png'; - default: - return null; - } - } - - static String titleForCurrency(CryptoCurrency cur) { - switch (cur) { - case CryptoCurrency.bnb: - return 'BNB'; - case CryptoCurrency.usdterc20: - return 'USDT'; - default: - return cur.title; - } - } - - static String descriptionForCurrency(CryptoCurrency cur) { - switch (cur) { - case CryptoCurrency.xmr: - return 'monero'; - case CryptoCurrency.ada: - return 'cardano'; - case CryptoCurrency.bch: - return 'bitcoin cash'; - case CryptoCurrency.bnb: - return 'binance bep2'; - case CryptoCurrency.btc: - return 'bitcoin'; - case CryptoCurrency.dai: - return 'dai eth'; - case CryptoCurrency.eth: - return 'ethereum'; - case CryptoCurrency.ltc: - return 'litecoin'; - case CryptoCurrency.trx: - return 'tron'; - case CryptoCurrency.usdt: - return 'usdt omni'; - case CryptoCurrency.usdterc20: - return 'tether ERC20 eth'; - case CryptoCurrency.xlm: - return 'lumens'; - case CryptoCurrency.xrp: - return 'ripple'; - default: - return cur.title; - } - } -} diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index a330675fd..ac21408a3 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -176,7 +176,7 @@ class ExchangeCardState extends State { padding: EdgeInsets.only(right: 5), child: widget.imageArrow, ), - Text(_selectedCurrency.toString() + ':', + Text(_selectedCurrency.toString(), style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, @@ -184,6 +184,40 @@ class ExchangeCardState extends State { ]), ), ), + _selectedCurrency.tag != null ? Padding( + padding: const EdgeInsets.only(right:3.0), + child: Container( + height: 32, + decoration: BoxDecoration( + color: widget.addressButtonsColor ?? Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Center( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text(_selectedCurrency.tag, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor)), + ), + ), + ), + ) : Container(), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Text(':', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white)), + ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index eb110f3b1..69858a3ac 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -173,85 +173,129 @@ class SendCardState extends State Observer( builder: (_) => Padding( padding: const EdgeInsets.only(top: 20), - child: Stack( - children: [ - BaseTextFormField( - focusNode: cryptoAmountFocus, - controller: cryptoAmountController, - keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) - ], - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: Text( - sendViewModel.selectedCryptoCurrency.title + - ':', + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Text( + sendViewModel.selectedCryptoCurrency.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + sendViewModel.selectedCryptoCurrency.tag != null ? Padding( + padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0), + child: Container( + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Center( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( sendViewModel.selectedCryptoCurrency.tag, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor)), + ), + ), + ), + ) : Container(), + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text(':', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white)), ), - suffixIcon: SizedBox( - width: prefixIconWidth, - ), - hintText: '0.0000', - borderColor: Theme.of(context) - .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: output.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel - .amountValidator), - if (!sendViewModel.isBatchSending) Positioned( - top: 2, - right: 0, - child: Container( - width: prefixIconWidth, - height: prefixIconHeight, - child: InkWell( - onTap: () async => - output.setSendAll(), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme - .display1 - .color, - borderRadius: - BorderRadius.all( - Radius.circular(6))), - child: Center( - child: Text( - S.of(context).all, - textAlign: - TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.bold, - color: - Theme.of(context) - .primaryTextTheme - .display1 - .decorationColor))), - ))))]) + ], + ), + ), + Expanded( + child: Stack( + children: [ + BaseTextFormField( + focusNode: cryptoAmountFocus, + controller: cryptoAmountController, + keyboardType: + TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) + ], + suffixIcon: SizedBox( + width: prefixIconWidth, + ), + hintText: '0.0000', + borderColor: Colors.transparent, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: output.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel + .amountValidator), + if (!sendViewModel.isBatchSending) Positioned( + top: 2, + right: 0, + child: Container( + width: prefixIconWidth, + height: prefixIconHeight, + child: InkWell( + onTap: () async => + output.setSendAll(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: + BorderRadius.all( + Radius.circular(6))), + child: Center( + child: Text( + S.of(context).all, + textAlign: + TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.bold, + color: + Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor))), + ))))]), + ), + ], + ) )), + Divider(height: 1,color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), Observer( builder: (_) => Padding( padding: EdgeInsets.only(top: 10), From a63c099f26d77c021e843a786fa20b2071230e7e Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 12:48:59 +0100 Subject: [PATCH 03/87] Add aditional check if wallet exists with special name --- lib/core/wallet_creation_service.dart | 22 ++++++++++++++++++- lib/di.dart | 3 ++- lib/view_model/wallet_creation_vm.dart | 13 ++++++----- lib/view_model/wallet_new_vm.dart | 10 ++++----- .../wallet_restoration_from_keys_vm.dart | 7 +++--- .../wallet_restoration_from_seed_vm.dart | 7 +++--- lib/view_model/wallet_restore_view_model.dart | 11 +++++----- 7 files changed, 45 insertions(+), 28 deletions(-) diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index dcb7983f5..636547308 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,6 +1,8 @@ import 'package:cake_wallet/di.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cw_core/wallet_base.dart'; @@ -14,7 +16,8 @@ class WalletCreationService { {WalletType initialType, this.secureStorage, this.keyService, - this.sharedPreferences}) + this.sharedPreferences, + this.walletInfoSource}) : type = initialType { if (type != null) { changeWalletType(type: type); @@ -25,6 +28,7 @@ class WalletCreationService { final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; final KeyService keyService; + final Box walletInfoSource; WalletService _service; void changeWalletType({@required WalletType type}) { @@ -32,7 +36,21 @@ class WalletCreationService { _service = getIt.get(param1: type); } + bool exists(String name) { + final walletName = name.toLowerCase(); + return walletInfoSource + .values + .any((walletInfo) => walletInfo.name.toLowerCase() == walletName); + } + + void checkIfExists(String name) { + if (exists(name)) { + throw Exception('Wallet with name ${name} already exists!'); + } + } + Future create(WalletCredentials credentials) async { + checkIfExists(credentials.name); final password = generateWalletPassword(type); credentials.password = password; await keyService.saveWalletPassword( @@ -41,6 +59,7 @@ class WalletCreationService { } Future restoreFromKeys(WalletCredentials credentials) async { + checkIfExists(credentials.name); final password = generateWalletPassword(type); credentials.password = password; await keyService.saveWalletPassword( @@ -49,6 +68,7 @@ class WalletCreationService { } Future restoreFromSeed(WalletCredentials credentials) async { + checkIfExists(credentials.name); final password = generateWalletPassword(type); credentials.password = password; await keyService.saveWalletPassword( diff --git a/lib/di.dart b/lib/di.dart index 7391e59ca..29fed4c56 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -215,7 +215,8 @@ Future setup( initialType: type, keyService: getIt.get(), secureStorage: getIt.get(), - sharedPreferences: getIt.get())); + sharedPreferences: getIt.get(), + walletInfoSource: _walletInfoSource)); getIt.registerFactoryParam((type, _) => WalletNewVM(getIt.get(), diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 8a4c76eb5..50629bb9b 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -15,7 +16,7 @@ part 'wallet_creation_vm.g.dart'; class WalletCreationVM = WalletCreationVMBase with _$WalletCreationVM; abstract class WalletCreationVMBase with Store { - WalletCreationVMBase(this._appStore, this._walletInfoSource, + WalletCreationVMBase(this._appStore, this._walletInfoSource, this.walletCreationService, {@required this.type, @required this.isRecovery}) { state = InitialExecutionState(); name = ''; @@ -29,14 +30,12 @@ abstract class WalletCreationVMBase with Store { WalletType type; final bool isRecovery; + final WalletCreationService walletCreationService; final Box _walletInfoSource; final AppStore _appStore; - bool nameExists(String name) { - final walletNameList = _walletInfoSource.values.map((e) => e.name.toLowerCase()).toList(); - - return walletNameList.contains(name.toLowerCase()); - } + bool nameExists(String name) + => walletCreationService.exists(name); Future create({dynamic options}) async { try { @@ -44,6 +43,8 @@ abstract class WalletCreationVMBase with Store { if (name?.isEmpty ?? true) { name = await generateName(); } + + walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); final credentials = getCredentials(options); diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index b044ea369..97a2aaee2 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -17,19 +17,17 @@ part 'wallet_new_vm.g.dart'; class WalletNewVM = WalletNewVMBase with _$WalletNewVM; abstract class WalletNewVMBase extends WalletCreationVM with Store { - WalletNewVMBase(AppStore appStore, this._walletCreationService, + WalletNewVMBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, {@required WalletType type}) : selectedMnemonicLanguage = '', - super(appStore, walletInfoSource, type: type, isRecovery: false); + super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false); @observable String selectedMnemonicLanguage; bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven; - final WalletCreationService _walletCreationService; - @override WalletCredentials getCredentials(dynamic options) { switch (type) { @@ -50,7 +48,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { @override Future process(WalletCredentials credentials) async { - _walletCreationService.changeWalletType(type: type); - return _walletCreationService.create(credentials); + walletCreationService.changeWalletType(type: type); + return walletCreationService.create(credentials); } } diff --git a/lib/view_model/wallet_restoration_from_keys_vm.dart b/lib/view_model/wallet_restoration_from_keys_vm.dart index 060745695..a51d8011c 100644 --- a/lib/view_model/wallet_restoration_from_keys_vm.dart +++ b/lib/view_model/wallet_restoration_from_keys_vm.dart @@ -20,9 +20,9 @@ class WalletRestorationFromKeysVM = WalletRestorationFromKeysVMBase abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM with Store { WalletRestorationFromKeysVMBase(AppStore appStore, - this._walletCreationService, Box walletInfoSource, + WalletCreationService walletCreationService, Box walletInfoSource, {@required WalletType type, @required this.language}) - : super(appStore, walletInfoSource, type: type, isRecovery: true); + : super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); @observable int height; @@ -42,7 +42,6 @@ abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM bool get hasRestorationHeight => type == WalletType.monero; final String language; - final WalletCreationService _walletCreationService; @override WalletCredentials getCredentials(dynamic options) { @@ -68,5 +67,5 @@ abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM @override Future process(WalletCredentials credentials) async => - _walletCreationService.restoreFromKeys(credentials); + walletCreationService.restoreFromKeys(credentials); } diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart index 1af730f13..ffd4184e0 100644 --- a/lib/view_model/wallet_restoration_from_seed_vm.dart +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -20,9 +20,9 @@ class WalletRestorationFromSeedVM = WalletRestorationFromSeedVMBase abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM with Store { WalletRestorationFromSeedVMBase(AppStore appStore, - this._walletCreationService, Box walletInfoSource, + WalletCreationService walletCreationService, Box walletInfoSource, {@required WalletType type, @required this.language, this.seed}) - : super(appStore, walletInfoSource, type: type, isRecovery: true); + : super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); @observable String seed; @@ -33,7 +33,6 @@ abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM bool get hasRestorationHeight => type == WalletType.monero; final String language; - final WalletCreationService _walletCreationService; @override WalletCredentials getCredentials(dynamic options) { @@ -53,5 +52,5 @@ abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM @override Future process(WalletCredentials credentials) async => - _walletCreationService.restoreFromSeed(credentials); + walletCreationService.restoreFromSeed(credentials); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 1943f76b9..83dcc57ce 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -22,7 +22,7 @@ class WalletRestoreViewModel = WalletRestoreViewModelBase with _$WalletRestoreViewModel; abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { - WalletRestoreViewModelBase(AppStore appStore, this._walletCreationService, + WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, {@required WalletType type}) : availableModes = (type == WalletType.monero || type == WalletType.haven) @@ -30,11 +30,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { : [WalletRestoreMode.seed], hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, - super(appStore, walletInfoSource, type: type, isRecovery: true) { + super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { isButtonEnabled = !hasSeedLanguageSelector && !hasBlockchainHeightLanguageSelector; mode = WalletRestoreMode.seed; - _walletCreationService.changeWalletType(type: type); + walletCreationService.changeWalletType(type: type); } static const moneroSeedMnemonicLength = 25; @@ -44,7 +44,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final List availableModes; final bool hasSeedLanguageSelector; final bool hasBlockchainHeightLanguageSelector; - final WalletCreationService _walletCreationService; @observable WalletRestoreMode mode; @@ -123,9 +122,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @override Future process(WalletCredentials credentials) async { if (mode == WalletRestoreMode.keys) { - return _walletCreationService.restoreFromKeys(credentials); + return walletCreationService.restoreFromKeys(credentials); } - return _walletCreationService.restoreFromSeed(credentials); + return walletCreationService.restoreFromSeed(credentials); } } From 9ea57146f77d55e48cbdb5af476d20a0835fefbb Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 12:56:37 +0100 Subject: [PATCH 04/87] Remove backups from menu --- lib/src/screens/dashboard/wallet_menu.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart index 704ddb2a6..b7d12a190 100644 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ b/lib/src/screens/dashboard/wallet_menu.dart @@ -52,24 +52,6 @@ class WalletMenu { image: Image.asset('assets/images/open_book_menu.png', height: 16, width: 16), handler: () => Navigator.of(context).pushNamed(Routes.addressBook)), - WalletMenuItem( - title: S.current.backup, - image: Image.asset('assets/images/restore_wallet.png', - height: 16, - width: 16, - color: Palette.darkBlue), - handler: () { - Navigator - .of(context) - .pushNamed( - Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { - if (isAuthenticatedSuccessfully) { - auth.close(); - Navigator.of(auth.context).pushNamed(Routes.backup); - } - }); - }), WalletMenuItem( title: S.current.settings_title, image: Image.asset('assets/images/settings_menu.png', From 8fec3272cf3d7f9d6a36e431580aacda636331d8 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 20:19:47 +0100 Subject: [PATCH 05/87] Temporarily remove ZZEC and TZEC --- cw_core/lib/crypto_currency.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 802ef5e3e..fead230d9 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -29,8 +29,8 @@ class CryptoCurrency extends EnumerableItem with Serializable { CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.xhv, - CryptoCurrency.zaddr, - CryptoCurrency.zec + //CryptoCurrency.zaddr, + //CryptoCurrency.zec ]; static const xmr = CryptoCurrency(title: 'XMR', iconPath: 'assets/images/monero_icon.png', name: 'Monero', raw: 0); static const ada = CryptoCurrency(title: 'ADA', iconPath: 'assets/images/ada_icon.png', name: 'Cardano', raw: 1); From b72443a8c4f2fb50e34a7e5510afb52cf13f366d Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Tue, 19 Jul 2022 15:29:28 +0100 Subject: [PATCH 06/87] Release 4.4.3 (#415) * Add ability for change password for wallets classes. * Update generateWalletPassword * Add WalletLoadingService * Add update monero password after wallet loading. * Update version for Cake Wallet to 4.4.2 (103) * Changed version for Cake Wallet to 4.4.3 (104). * Changed version for Cake Wallet android * Changed version for Monero.com ios and android. --- .../lib/electrum_transaction_history.dart | 7 ++- cw_bitcoin/lib/electrum_wallet.dart | 9 +++- cw_core/lib/wallet_base.dart | 2 + cw_haven/ios/Classes/haven_api.cpp | 10 ++++ cw_haven/lib/api/signatures.dart | 2 + cw_haven/lib/api/types.dart | 2 + cw_haven/lib/api/wallet.dart | 19 +++++++ cw_haven/lib/haven_wallet.dart | 5 ++ cw_monero/ios/Classes/monero_api.cpp | 10 ++++ cw_monero/lib/api/signatures.dart | 2 + cw_monero/lib/api/types.dart | 2 + cw_monero/lib/api/wallet.dart | 19 +++++++ cw_monero/lib/monero_wallet.dart | 5 ++ lib/core/generate_wallet_password.dart | 10 +--- lib/core/wallet_creation_service.dart | 42 ++++++++++++--- lib/core/wallet_loading_service.dart | 52 +++++++++++++++++++ lib/di.dart | 9 +++- lib/entities/load_current_wallet.dart | 7 ++- lib/entities/preferences_key.dart | 4 ++ .../wallet_list/wallet_list_view_model.dart | 15 +++--- .../wallet_restoration_from_keys_vm.dart | 2 +- .../wallet_restoration_from_seed_vm.dart | 2 +- lib/view_model/wallet_restore_view_model.dart | 2 +- scripts/android/app_env.sh | 8 +-- scripts/ios/app_env.sh | 8 +-- 25 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 lib/core/wallet_loading_service.dart diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index d1459d7f7..94f328900 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -24,7 +24,7 @@ abstract class ElectrumTransactionHistoryBase } final WalletInfo walletInfo; - final String _password; + String _password; int _height; Future init() async => await _load(); @@ -51,6 +51,11 @@ abstract class ElectrumTransactionHistoryBase } } + Future changePassword(String password) async { + _password = password; + await save(); + } + Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2902a21ca..f6d3f30f8 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -109,7 +109,7 @@ abstract class ElectrumWalletBase extends WalletBase BitcoinWalletKeys( wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); - final String _password; + String _password; List unspentCoins; List _feeRates; Map> _scripthashesUpdateSubject; @@ -392,6 +392,13 @@ abstract class ElectrumWalletBase extends WalletBase changePassword(String password) async { + _password = password; + await save(); + await transactionHistory.changePassword(password); + } + bitcoin.ECPair keyPairFor({@required int index}) => generateKeyPair(hd: hd, index: index, network: networkType); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index b8c1aaa45..173ba8155 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -69,4 +69,6 @@ abstract class WalletBase< Future rescan({int height}); void close(); + + Future changePassword(String password); } diff --git a/cw_haven/ios/Classes/haven_api.cpp b/cw_haven/ios/Classes/haven_api.cpp index 92bca4294..c1013bf87 100644 --- a/cw_haven/ios/Classes/haven_api.cpp +++ b/cw_haven/ios/Classes/haven_api.cpp @@ -565,6 +565,16 @@ extern "C" store_lock.unlock(); } + bool set_password(char *password, Utf8Box &error) { + bool is_changed = get_current_wallet()->setPassword(std::string(password)); + + if (!is_changed) { + error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); + } + + return is_changed; + } + bool transaction_create(char *address, char *asset_type, char *payment_id, char *amount, uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) { diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart index c9db9ac8d..9dd1c8dac 100644 --- a/cw_haven/lib/api/signatures.dart +++ b/cw_haven/lib/api/signatures.dart @@ -51,6 +51,8 @@ typedef set_recovering_from_seed = Void Function(Int8); typedef store_c = Void Function(Pointer); +typedef set_password = Int8 Function(Pointer password, Pointer error); + typedef set_listener = Void Function(); typedef get_syncing_height = Int64 Function(); diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart index 09b6f77e0..878297501 100644 --- a/cw_haven/lib/api/types.dart +++ b/cw_haven/lib/api/types.dart @@ -51,6 +51,8 @@ typedef SetRecoveringFromSeed = void Function(int); typedef Store = void Function(Pointer); +typedef SetPassword = int Function(Pointer password, Pointer error); + typedef SetListener = void Function(); typedef GetSyncingHeight = int Function(); diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart index e490743d2..3370fd3e0 100644 --- a/cw_haven/lib/api/wallet.dart +++ b/cw_haven/lib/api/wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'package:cw_haven/api/structs/ut8_box.dart'; import 'package:cw_haven/api/convert_utf8_to_string.dart'; import 'package:cw_haven/api/signatures.dart'; import 'package:cw_haven/api/types.dart'; @@ -67,6 +68,9 @@ final setRecoveringFromSeedNative = havenApi final storeNative = havenApi.lookup>('store').asFunction(); +final setPasswordNative = + havenApi.lookup>('set_password').asFunction(); + final setListenerNative = havenApi .lookup>('set_listener') .asFunction(); @@ -193,6 +197,21 @@ void storeSync() { free(pathPointer); } +void setPasswordSync(String password) { + final passwordPointer = Utf8.toUtf8(password); + final errorMessagePointer = allocate(); + final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; + free(passwordPointer); + + if (!changed) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw Exception(message); + } + + free(errorMessagePointer); +} + void closeCurrentWallet() => closeCurrentWalletNative(); String getSecretViewKey() => diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 7fb8ed157..c107d2f52 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -240,6 +240,11 @@ abstract class HavenWalletBase extends WalletBase changePassword(String password) async { + haven_wallet.setPasswordSync(password); + } + Future getNodeHeight() async => haven_wallet.getNodeHeight(); Future isConnected() async => haven_wallet.isConnected(); diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index f81f63d16..4d3f5f56b 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -467,6 +467,16 @@ extern "C" store_lock.unlock(); } + bool set_password(char *password, Utf8Box &error) { + bool is_changed = get_current_wallet()->setPassword(std::string(password)); + + if (!is_changed) { + error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); + } + + return is_changed; + } + bool transaction_create(char *address, char *payment_id, char *amount, uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) { diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 9781aff2e..16f983480 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -47,6 +47,8 @@ typedef set_recovering_from_seed = Void Function(Int8); typedef store_c = Void Function(Pointer); +typedef set_password = Int8 Function(Pointer password, Pointer error); + typedef set_listener = Void Function(); typedef get_syncing_height = Int64 Function(); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 4caa1283f..3438b89fc 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -47,6 +47,8 @@ typedef SetRecoveringFromSeed = void Function(int); typedef Store = void Function(Pointer); +typedef SetPassword = int Function(Pointer password, Pointer error); + typedef SetListener = void Function(); typedef GetSyncingHeight = int Function(); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 72507e912..9e84d7865 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:cw_monero/api/convert_utf8_to_string.dart'; import 'package:cw_monero/api/signatures.dart'; import 'package:cw_monero/api/types.dart'; @@ -67,6 +68,9 @@ final setRecoveringFromSeedNative = moneroApi final storeNative = moneroApi.lookup>('store').asFunction(); +final setPasswordNative = + moneroApi.lookup>('set_password').asFunction(); + final setListenerNative = moneroApi .lookup>('set_listener') .asFunction(); @@ -197,6 +201,21 @@ void storeSync() { free(pathPointer); } +void setPasswordSync(String password) { + final passwordPointer = Utf8.toUtf8(password); + final errorMessagePointer = allocate(); + final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; + free(passwordPointer); + + if (!changed) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw Exception(message); + } + + free(errorMessagePointer); +} + void closeCurrentWallet() => closeCurrentWalletNative(); String getSecretViewKey() => diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 6ac31c9f0..175bd96f5 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -258,6 +258,11 @@ abstract class MoneroWalletBase extends WalletBase changePassword(String password) async { + monero_wallet.setPasswordSync(password); + } + Future getNodeHeight() async => monero_wallet.getNodeHeight(); Future isConnected() async => monero_wallet.isConnected(); diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart index 71fb68d9c..c9a9fac57 100644 --- a/lib/core/generate_wallet_password.dart +++ b/lib/core/generate_wallet_password.dart @@ -1,12 +1,6 @@ import 'package:uuid/uuid.dart'; import 'package:cw_core/key.dart'; -import 'package:cw_core/wallet_type.dart'; -String generateWalletPassword(WalletType type) { - switch (type) { - case WalletType.monero: - return Uuid().v4(); - default: - return generateKey(); - } +String generateWalletPassword() { + return generateKey(); } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 636547308..9c37657f2 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; @@ -31,6 +32,8 @@ class WalletCreationService { final Box walletInfoSource; WalletService _service; + static const _isNewMoneroWalletPasswordUpdated = true; + void changeWalletType({@required WalletType type}) { this.type = type; _service = getIt.get(param1: type); @@ -51,28 +54,55 @@ class WalletCreationService { Future create(WalletCredentials credentials) async { checkIfExists(credentials.name); - final password = generateWalletPassword(type); + final password = generateWalletPassword(); credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - return await _service.create(credentials); + final wallet = await _service.create(credentials); + + if (wallet.type == WalletType.monero) { + await sharedPreferences + .setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), + _isNewMoneroWalletPasswordUpdated); + } + + return wallet; } Future restoreFromKeys(WalletCredentials credentials) async { checkIfExists(credentials.name); - final password = generateWalletPassword(type); + final password = generateWalletPassword(); credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - return await _service.restoreFromKeys(credentials); + final wallet = await _service.restoreFromKeys(credentials); + + if (wallet.type == WalletType.monero) { + await sharedPreferences + .setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), + _isNewMoneroWalletPasswordUpdated); + } + + return wallet; } Future restoreFromSeed(WalletCredentials credentials) async { checkIfExists(credentials.name); - final password = generateWalletPassword(type); + final password = generateWalletPassword(); credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - return await _service.restoreFromSeed(credentials); + final wallet = await _service.restoreFromSeed(credentials); + + if (wallet.type == WalletType.monero) { + await sharedPreferences + .setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), + _isNewMoneroWalletPasswordUpdated); + } + + return wallet; } } diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart new file mode 100644 index 000000000..844e5c1ca --- /dev/null +++ b/lib/core/wallet_loading_service.dart @@ -0,0 +1,52 @@ +import 'package:cake_wallet/core/generate_wallet_password.dart'; +import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class WalletLoadingService { + WalletLoadingService( + this.sharedPreferences, + this.keyService, + this.walletServiceFactory); + + final SharedPreferences sharedPreferences; + final KeyService keyService; + final WalletService Function(WalletType type) walletServiceFactory; + + Future load(WalletType type, String name) async { + if (walletServiceFactory == null) { + throw Exception('WalletLoadingService.walletServiceFactory is not set'); + } + final walletService = walletServiceFactory?.call(type); + final password = await keyService.getWalletPassword(walletName: name); + final wallet = await walletService.openWallet(name, password); + + if (type == WalletType.monero) { + await upateMoneroWalletPassword(wallet); + } + + return wallet; + } + + Future upateMoneroWalletPassword(WalletBase wallet) async { + final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name); + var isPasswordUpdated = sharedPreferences.getBool(key) ?? false; + + if (isPasswordUpdated) { + return; + } + + final password = generateWalletPassword(); + // Save new generated password with backup key for case + // if wallet will change password, but it will faild to updated in secure storage + final bakWalletName = '#__${wallet.name}_bak__#'; + await keyService.saveWalletPassword(walletName: bakWalletName, password: password); + await wallet.changePassword(password); + await keyService.saveWalletPassword(walletName: wallet.name, password: password); + isPasswordUpdated = true; + await sharedPreferences.setBool(key, isPasswordUpdated); + } +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 29fed4c56..2270f1863 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -124,6 +124,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; +import 'package:cake_wallet/core/wallet_loading_service.dart'; final getIt = GetIt.instance; @@ -218,6 +219,12 @@ Future setup( sharedPreferences: getIt.get(), walletInfoSource: _walletInfoSource)); + getIt.registerFactory( + () => WalletLoadingService( + getIt.get(), + getIt.get(), + (WalletType type) => getIt.get(param1: type))); + getIt.registerFactoryParam((type, _) => WalletNewVM(getIt.get(), getIt.get(param1: type), _walletInfoSource, @@ -352,7 +359,7 @@ Future setup( getIt.registerFactory(() => WalletListViewModel( _walletInfoSource, getIt.get(), - getIt.get())); + getIt.get())); getIt.registerFactory(() => WalletListPage(walletListViewModel: getIt.get())); diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 6751b7709..56502cc59 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/key_service.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/core/wallet_loading_service.dart'; Future loadCurrentWallet() async { final appStore = getIt.get(); @@ -15,9 +16,7 @@ Future loadCurrentWallet() async { getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final type = deserializeFromInt(typeRaw); - final password = - await getIt.get().getWalletPassword(walletName: name); - final _service = getIt.get(param1: type); - final wallet = await _service.openWallet(name, password); + final walletLoadingService = getIt.get(); + final wallet = await walletLoadingService.load(type, name); appStore.changeCurrentWallet(wallet); } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 82e100c42..f4a0008a2 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -22,4 +22,8 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; + static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; + + static String moneroWalletUpdateV1Key(String name) + => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 67a80db8f..1afe3347d 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,9 +1,9 @@ +import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/core/key_service.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cw_core/wallet_info.dart'; @@ -16,7 +16,7 @@ class WalletListViewModel = WalletListViewModelBase with _$WalletListViewModel; abstract class WalletListViewModelBase with Store { WalletListViewModelBase(this._walletInfoSource, this._appStore, - this._keyService) { + this._walletLoadingService) { wallets = ObservableList(); _updateList(); } @@ -26,17 +26,14 @@ abstract class WalletListViewModelBase with Store { final AppStore _appStore; final Box _walletInfoSource; - final KeyService _keyService; + final WalletLoadingService _walletLoadingService; WalletType get currentWalletType => _appStore.wallet.type; @action - Future loadWallet(WalletListItem wallet) async { - final password = - await _keyService.getWalletPassword(walletName: wallet.name); - final walletService = getIt.get(param1: wallet.type); - final loadedWallet = await walletService.openWallet(wallet.name, password); - _appStore.changeCurrentWallet(loadedWallet); + Future loadWallet(WalletListItem walletItem) async { + final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); + _appStore.changeCurrentWallet(wallet); _updateList(); } diff --git a/lib/view_model/wallet_restoration_from_keys_vm.dart b/lib/view_model/wallet_restoration_from_keys_vm.dart index a51d8011c..ae51bc833 100644 --- a/lib/view_model/wallet_restoration_from_keys_vm.dart +++ b/lib/view_model/wallet_restoration_from_keys_vm.dart @@ -45,7 +45,7 @@ abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM @override WalletCredentials getCredentials(dynamic options) { - final password = generateWalletPassword(type); + final password = generateWalletPassword(); switch (type) { case WalletType.monero: diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart index ffd4184e0..a4fb3c34a 100644 --- a/lib/view_model/wallet_restoration_from_seed_vm.dart +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -36,7 +36,7 @@ abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM @override WalletCredentials getCredentials(dynamic options) { - final password = generateWalletPassword(type); + final password = generateWalletPassword(); switch (type) { case WalletType.monero: diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 83dcc57ce..ce6546655 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -53,7 +53,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @override WalletCredentials getCredentials(dynamic options) { - final password = generateWalletPassword(type); + final password = generateWalletPassword(); final height = options['height'] as int; name = options['name'] as String; diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 689b866eb..9245c9c1f 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.6" -MONERO_COM_BUILD_NUMBER=13 +MONERO_COM_VERSION="1.0.7" +MONERO_COM_BUILD_NUMBER=14 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.1" -CAKEWALLET_BUILD_NUMBER=104 +CAKEWALLET_VERSION="4.4.3" +CAKEWALLET_BUILD_NUMBER=105 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index ca7e5de70..de6d2adf8 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.6" -MONERO_COM_BUILD_NUMBER=16 +MONERO_COM_VERSION="1.0.7" +MONERO_COM_BUILD_NUMBER=17 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.1" -CAKEWALLET_BUILD_NUMBER=100 +CAKEWALLET_VERSION="4.4.3" +CAKEWALLET_BUILD_NUMBER=104 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 418c9563fe4997e953ab67910514d7998a2c2cfa Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 28 Jul 2022 18:03:16 +0100 Subject: [PATCH 07/87] Ionia (#437) * Initial ionia service * Ionia manage card UI (#374) * design ui for cakepay * Add manage cards page ui * create auth ui for ionia * add authentication logic * implement user create card * Add ionia merchant sevic * Add anypay. Add purschase gift card. * display virtual card (#385) * display virtual card * fix formatting * Remove IoniaMerchantService from IoniaViewModel * Add hex and txKey for monero pending transaction. * Changed monero version and monero repo to cake tech. * Add anypay payment. Add filter by search for ionia, add get purchased items for ionia. * Fix for get transactions for hidden addresses for electrum wallet * Add ionia categories. * Add anypay commited info for payments. * Update UI with new fixes (#400) * Change ionia base url. Add exception throwing for error messaging for some of ionia calls. * CW-102 fix logic for ionia issues (#403) * refactor tips (#406) * refactor tips * refactor ionia tips implementation * Cw 115 implement gift cards list for ionia (#405) * Implement show purchased cards * fix padding * Fixes for getting of purchased gift cards. * Implement gift card details screen (#408) * Implement gift card details screen * Add redeem for ionia gift cards * Fix navigation after ionia opt redirection. * Fix update gift cards list. * Add payment status update for ionia. * Add usage instruction to gift card. * Add copy for ionia gift card info. * Change version for Cake Wallet ios. * Add localisation (#414) * Fixes for fiat amounts for ionia. * CW-128 marketplace screen text changes (#416) * Change text on marketplace * fix build issues * fix build * UI fixes for ionia. * UI fixes for ionia. (#421) * CW-129 ionia welcome screen text changes (#418) * update welcome text * Update localization * Cw 133 (#422) * UI fixes for ionia. * Fixes for display card item on gift cards screen. * Fix signup page (#419) * Changed tips for ionia. * Cw 132 (#425) * UI fixes for ionia. * Changed tips for ionia. * Cw 131 (#426) * UI fixes for ionia. * Changed tips for ionia. * Fixes for IoniaBuyGiftCardDetailPage screen. Renamed 'Manage Cards' to 'Gift Cards'. Hide discount badge label for 0 discount. * Change ionia heading font style (#427) * Fix for AddressResolver in di * Changed build number for Cake Wallet ios. * fix currency format for card details and routing for mark as redeemed (#431) * fix terms and condition overflow in ionia (#430) * fix terms and condition scroll * fix color issues * reuse * refactor widget * Remove IoniaTokenService * Change api for ionia to staging * Update versions for Cake Wallet for android and ios. * Fixes for instructions. Remove diplay error on payment status screen. * Change build versions for Cake Wallet * Add ionia sign in. * Update for discounts and statuses for ionia merch. * Fixes for qr/barcode on ionia gift card screen. * Fixed formatting for display ionia discounts. * Fix merchant.discount.toStringAsFixed issue * Add savingsPercentage to ionia merch discount. * Change build number for Cake Wallet ios and android. * Disable ionia for haven (#440) Co-authored-by: Godwin Asuquo <41484542+godilite@users.noreply.github.com> --- assets/images/airplane.png | Bin 0 -> 960 bytes assets/images/badge_discount.png | Bin 0 -> 1411 bytes assets/images/card.png | Bin 0 -> 556 bytes assets/images/category.png | Bin 0 -> 454 bytes assets/images/copy.png | Bin 0 -> 556 bytes assets/images/delivery.png | Bin 0 -> 728 bytes assets/images/filter.png | Bin 0 -> 504 bytes assets/images/food.png | Bin 0 -> 710 bytes assets/images/gaming.png | Bin 0 -> 705 bytes assets/images/global.png | Bin 0 -> 997 bytes assets/images/mastercard.png | Bin 0 -> 2114 bytes assets/images/mini_search_icon.png | Bin 0 -> 536 bytes assets/images/profile.png | Bin 0 -> 1060 bytes assets/images/red_badge_discount.png | Bin 0 -> 1506 bytes assets/images/tshirt.png | Bin 0 -> 583 bytes assets/images/wifi.png | Bin 0 -> 1087 bytes .../lib/bitcoin_transaction_credentials.dart | 5 +- cw_bitcoin/lib/electrum_wallet.dart | 106 ++-- .../lib/pending_bitcoin_transaction.dart | 3 + cw_core/lib/pending_transaction.dart | 1 + cw_haven/lib/pending_haven_transaction.dart | 3 + cw_haven/pubspec.lock | 7 - cw_monero/ios/Classes/monero_api.cpp | 6 +- .../lib/api/structs/pending_transaction.dart | 18 +- cw_monero/lib/api/transaction_history.dart | 4 + cw_monero/lib/pending_monero_transaction.dart | 5 + ios/Podfile.lock | 6 + lib/anypay/any_pay_chain.dart | 5 + lib/anypay/any_pay_payment.dart | 64 +++ .../any_pay_payment_committed_info.dart | 17 + lib/anypay/any_pay_payment_instruction.dart | 32 ++ .../any_pay_payment_instruction_output.dart | 10 + lib/anypay/any_pay_trasnaction.dart | 9 + lib/anypay/anypay_api.dart | 92 ++++ lib/bitcoin/cw_bitcoin.dart | 12 +- lib/core/email_validator.dart | 11 + lib/di.dart | 130 ++++- lib/ionia/ionia_any_pay_payment_info.dart | 9 + lib/ionia/ionia_anypay.dart | 92 ++++ lib/ionia/ionia_api.dart | 444 ++++++++++++++++ lib/ionia/ionia_category.dart | 18 + lib/ionia/ionia_create_state.dart | 58 +++ lib/ionia/ionia_gift_card.dart | 69 +++ lib/ionia/ionia_gift_card_instruction.dart | 28 + lib/ionia/ionia_merchant.dart | 176 +++++++ lib/ionia/ionia_order.dart | 23 + lib/ionia/ionia_service.dart | 172 ++++++ lib/ionia/ionia_tip.dart | 12 + lib/ionia/ionia_token_data.dart | 43 ++ lib/ionia/ionia_user_credentials.dart | 6 + lib/ionia/ionia_virtual_card.dart | 43 ++ lib/main.dart | 5 +- lib/monero/cw_monero.dart | 58 ++- lib/router.dart | 60 +++ lib/routes.dart | 16 +- lib/src/screens/dashboard/dashboard_page.dart | 10 +- .../dashboard/widgets/market_place_page.dart | 80 +++ .../ionia/auth/ionia_create_account_page.dart | 154 ++++++ .../screens/ionia/auth/ionia_login_page.dart | 112 ++++ .../ionia/auth/ionia_verify_otp_page.dart | 151 ++++++ .../ionia/auth/ionia_welcome_page.dart | 104 ++++ .../ionia/cards/ionia_account_cards_page.dart | 181 +++++++ .../ionia/cards/ionia_account_page.dart | 181 +++++++ .../cards/ionia_activate_debit_card_page.dart | 114 ++++ .../cards/ionia_buy_card_detail_page.dart | 492 ++++++++++++++++++ .../ionia/cards/ionia_buy_gift_card.dart | 185 +++++++ .../ionia/cards/ionia_custom_tip_page.dart | 176 +++++++ .../ionia/cards/ionia_debit_card_page.dart | 379 ++++++++++++++ .../cards/ionia_gift_card_detail_page.dart | 188 +++++++ .../ionia/cards/ionia_manage_cards_page.dart | 340 ++++++++++++ .../cards/ionia_payment_status_page.dart | 217 ++++++++ lib/src/screens/ionia/ionia.dart | 9 + lib/src/screens/ionia/widgets/card_item.dart | 139 +++++ lib/src/screens/ionia/widgets/card_menu.dart | 11 + .../screens/ionia/widgets/confirm_modal.dart | 148 ++++++ .../ionia/widgets/ionia_alert_model.dart | 86 +++ .../ionia/widgets/ionia_filter_modal.dart | 134 +++++ lib/src/screens/ionia/widgets/ionia_tile.dart | 44 ++ .../ionia/widgets/rounded_checkbox.dart | 27 + .../ionia/widgets/text_icon_button.dart | 35 ++ lib/src/widgets/alert_with_two_actions.dart | 16 +- lib/src/widgets/base_alert_dialog.dart | 114 ++-- lib/src/widgets/cake_scrollbar.dart | 27 +- lib/src/widgets/discount_badge.dart | 35 ++ lib/src/widgets/market_place_item.dart | 66 +++ lib/typography.dart | 59 +++ .../ionia/ionia_account_view_model.dart | 44 ++ .../ionia/ionia_auth_view_model.dart | 67 +++ .../ionia/ionia_buy_card_view_model.dart | 31 ++ .../ionia/ionia_filter_view_model.dart | 58 +++ .../ionia_gift_card_details_view_model.dart | 35 ++ .../ionia_gift_cards_list_view_model.dart | 103 ++++ .../ionia_payment_status_view_model.dart | 58 +++ .../ionia_purchase_merch_view_model.dart | 94 ++++ lib/view_model/send/send_view_model.dart | 4 +- res/values/strings_de.arb | 100 +++- res/values/strings_en.arb | 102 +++- res/values/strings_es.arb | 100 +++- res/values/strings_fr.arb | 102 +++- res/values/strings_hi.arb | 100 +++- res/values/strings_hr.arb | 100 +++- res/values/strings_it.arb | 100 +++- res/values/strings_ja.arb | 100 +++- res/values/strings_ko.arb | 100 +++- res/values/strings_nl.arb | 100 +++- res/values/strings_pl.arb | 100 +++- res/values/strings_pt.arb | 100 +++- res/values/strings_ru.arb | 100 +++- res/values/strings_uk.arb | 100 +++- res/values/strings_zh.arb | 100 +++- scripts/android/app_env.sh | 4 +- scripts/android/build_monero.sh | 2 +- scripts/ios/app_env.sh | 4 +- scripts/ios/build_monero.sh | 4 +- tool/configure.dart | 6 +- 115 files changed, 7726 insertions(+), 184 deletions(-) create mode 100644 assets/images/airplane.png create mode 100644 assets/images/badge_discount.png create mode 100644 assets/images/card.png create mode 100644 assets/images/category.png create mode 100644 assets/images/copy.png create mode 100644 assets/images/delivery.png create mode 100644 assets/images/filter.png create mode 100644 assets/images/food.png create mode 100644 assets/images/gaming.png create mode 100644 assets/images/global.png create mode 100644 assets/images/mastercard.png create mode 100644 assets/images/mini_search_icon.png create mode 100644 assets/images/profile.png create mode 100644 assets/images/red_badge_discount.png create mode 100644 assets/images/tshirt.png create mode 100644 assets/images/wifi.png create mode 100644 lib/anypay/any_pay_chain.dart create mode 100644 lib/anypay/any_pay_payment.dart create mode 100644 lib/anypay/any_pay_payment_committed_info.dart create mode 100644 lib/anypay/any_pay_payment_instruction.dart create mode 100644 lib/anypay/any_pay_payment_instruction_output.dart create mode 100644 lib/anypay/any_pay_trasnaction.dart create mode 100644 lib/anypay/anypay_api.dart create mode 100644 lib/core/email_validator.dart create mode 100644 lib/ionia/ionia_any_pay_payment_info.dart create mode 100644 lib/ionia/ionia_anypay.dart create mode 100644 lib/ionia/ionia_api.dart create mode 100644 lib/ionia/ionia_category.dart create mode 100644 lib/ionia/ionia_create_state.dart create mode 100644 lib/ionia/ionia_gift_card.dart create mode 100644 lib/ionia/ionia_gift_card_instruction.dart create mode 100644 lib/ionia/ionia_merchant.dart create mode 100644 lib/ionia/ionia_order.dart create mode 100644 lib/ionia/ionia_service.dart create mode 100644 lib/ionia/ionia_tip.dart create mode 100644 lib/ionia/ionia_token_data.dart create mode 100644 lib/ionia/ionia_user_credentials.dart create mode 100644 lib/ionia/ionia_virtual_card.dart create mode 100644 lib/src/screens/dashboard/widgets/market_place_page.dart create mode 100644 lib/src/screens/ionia/auth/ionia_create_account_page.dart create mode 100644 lib/src/screens/ionia/auth/ionia_login_page.dart create mode 100644 lib/src/screens/ionia/auth/ionia_verify_otp_page.dart create mode 100644 lib/src/screens/ionia/auth/ionia_welcome_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_account_cards_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_account_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_buy_gift_card.dart create mode 100644 lib/src/screens/ionia/cards/ionia_custom_tip_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_debit_card_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_manage_cards_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_payment_status_page.dart create mode 100644 lib/src/screens/ionia/ionia.dart create mode 100644 lib/src/screens/ionia/widgets/card_item.dart create mode 100644 lib/src/screens/ionia/widgets/card_menu.dart create mode 100644 lib/src/screens/ionia/widgets/confirm_modal.dart create mode 100644 lib/src/screens/ionia/widgets/ionia_alert_model.dart create mode 100644 lib/src/screens/ionia/widgets/ionia_filter_modal.dart create mode 100644 lib/src/screens/ionia/widgets/ionia_tile.dart create mode 100644 lib/src/screens/ionia/widgets/rounded_checkbox.dart create mode 100644 lib/src/screens/ionia/widgets/text_icon_button.dart create mode 100644 lib/src/widgets/discount_badge.dart create mode 100644 lib/src/widgets/market_place_item.dart create mode 100644 lib/typography.dart create mode 100644 lib/view_model/ionia/ionia_account_view_model.dart create mode 100644 lib/view_model/ionia/ionia_auth_view_model.dart create mode 100644 lib/view_model/ionia/ionia_buy_card_view_model.dart create mode 100644 lib/view_model/ionia/ionia_filter_view_model.dart create mode 100644 lib/view_model/ionia/ionia_gift_card_details_view_model.dart create mode 100644 lib/view_model/ionia/ionia_gift_cards_list_view_model.dart create mode 100644 lib/view_model/ionia/ionia_payment_status_view_model.dart create mode 100644 lib/view_model/ionia/ionia_purchase_merch_view_model.dart diff --git a/assets/images/airplane.png b/assets/images/airplane.png new file mode 100644 index 0000000000000000000000000000000000000000..6be73be94371c0f3790538bcc513affe9c231ddb GIT binary patch literal 960 zcmV;x13&zUP)*f zDF=)<%Z2F4HC_s3-#GKST}as$N%SS{dpmDtzIij>n+Ncp2Sy1;rt&HDDT5g6C!$1G z2Z$b#f2a}Mz2d7c+dhs>#G@l9q zK#}>VtiSveW%!Vz7b8CKf={%-5hh0`-zB4TgVcD4lnDX&qo4n)aLLf5Jr!#fote{( zT}%+=$igz8gse>*J5ekc}zm@C><8yR|r6a zhhosKXxp~KlcNBq3H1{xr1|k$`zY*jg2J{TAX8h{LI5O^{+(zwsMcsS8lvQBpaM#v zxv+94r0Cq5rL%hNc@G+=sgcYv`8)cuRqGpk{G6SnZry#f{{5?47rD%R_s4$l@*bdC zs%*^Xjt;imJA-4K1-Q+!m7{~t+vihi+1923F3!GarYjf{Ju1WRng)KABA&l)quEwq zS<=H)AKwXHHEcg#>s2xWe%)Bb>+0FG3y`8p4Qou>?D@1sKKceo`6+&Y`r(pCpCcB^ i9x_o%SAAyx$@l{r{hQSsZ4&SR0000Q{Nhzs%n4Xya)KgVSnJKB> zQ&}&(`}4Kf^+ABYL{%kTSpPac={>{zeXI zn{!>RxBW1mGuOh{5W|UWck|hHy|rb;{DKr#1TL0gOq*UZJS=bCj`369(fLK5vP`kGLgLSOg7wqvkX#~wrs$mIjq^{FD7(v6_| zdu))%)bG7~NNd|z(Bjh&k^txrq-7Y+4N3ZSn{wwo^F>i>WYFQq%!e4ayha9GkfylWc*#;-~ zjSsl|x2kp`KvG$WX5}v)AHJ@E|K#OYq9M(QE22|9hvz4LCsE3u@OPdkjO&`!HAJpv zo)~@3{TT?z4DS5kzE&ybqgMs;)4q%V@p8~w27$Vv9Un5Si5WCn5qFX;;8H7kIE~2p z=XRMUnLbtXMoRcRYk8MyL`L9$Tp!o_lT;oHx|nrUng}C{NClcsTB5VrDJP;pM@r~j ze`5xuajBvm)t%>ay_R*@xtu)>x~hlgEVg@7B}yH*et23z<6{~(L=1kg00xo%(;+h( zjN>S7!uphs7}CM8aF6yn+i}b5wJq)nSTmi09w~D&o}uoY_a8%R#yyVfOR=EIx`mXF z4bEyS&5xG!nt9BUr!gn-SwK+XBMV7d>yAQ+!xd&fO4@sN6=x^Bo|6*7;#tTPd@f$cfEQ+`z% zvU7M|Youae>J>(2J9}73@of$N&GI)788QR8XKLi3pO)Bu$WRuSR)mad))g69Q_%bC z33LH+6xmc()g+Z_jiZ4DBxGfdtI7Z%ea{2p#iLY! z8Y>sWEL08P%*l`$UxnsvBiR*_$5t!Z$;{*BvK1>V3?($_ z1v#(eTYV7pNeiBQtIA#mk=5R zFOZGI#WEEbBC023s*H7K9he$sGUb?diIMTiD(wcy^6KSF=|eW`Dn3ICZndYes$VyI z^qUry%9NHnrrL|K%s{^*lOfYeVRII=z)G1}tECS$6Bt*K#J!}>>y%*?D~-!BnBT_@ z@_dv4ex)2!>W2U6kL3-&R{eB1N;i9PP3NSfiz2n=FrnkPqd z&6vXxcRuNyEQaFyfkkFUJ~W{}>W7$H#O)Wy6ONGRc6C%&tya=cN91`=<{=>-4)8Y0 zjzV+*HgcF0B5p>QN80>JwXBx8ub~(TuB-$H@2MA$=Ss#O*N^YB=la=LDA%|Z2}X4m z9NNT@v{J*0sagYI2NI@|&6a@LL#>KeR^d8}D7uz3h#f8mGr^Bw6Y#9|zW`BSjzW_V R`&R$}002ovPDHLkV1hR^mL32A literal 0 HcmV?d00001 diff --git a/assets/images/card.png b/assets/images/card.png new file mode 100644 index 0000000000000000000000000000000000000000..58935bdaca99aca87b6ee1888f90f0cf01df986f GIT binary patch literal 556 zcmV+{0@MA8P)8^9)%Nq|XcCx8uNCLl}@?!ZBp#C8&@#!lrYooYEh(w%)y zsvjVcNX(k7Pf%5rqMr@Lib7!;w23myvUiMyl3r0fP<&S5DXQOGlr7iHk~|AvG2Lnp zMN{l)N3@)56yQj{q&S-24@|?_JU5@*KrDuLrIIG~(mSHYp2`r+uoY%^0*8t9r0> zh(sFxZoDb+mo&WPV~AyAR^3}%-i#`Z=qB`Xi@>8 zNd<%^6%d+KKxmd@#Ox5D>lYRDzv-n%1K&O8Ft`Ad*puTLI3b_Lbq~(us#-|8UF)`x zR_mEp4W1VAXyCeM1PYd1T?O300w;(bhG_nYT-2g-mCd3V8C#nnq8yQ6{Lw$V-6uTA zU5LRSHNL3R+-=+IY;L`wdtlgV)R@*@=Uj4h?e)etg4uuI=)4DvU{CRgI6PAB0M2FG uPM#U^k&jQj!t@{cep=U-bRvvt&t0000?~HMP zlqnVy7j61h|G}I8UjD!bSauLu>>XX5BC;NTV3t&fQIdcg1>LcrR@>h(QgY2t+a)PL zOab~&gN`RS&EK%TviWxmV(A;ykyK}JqH6w=*yJW>l2ce1%v@ZX2d!c7H>?e681PTA zT1a0rX-X@#9AjDNvd}beQ>6Qd>2^uZAbcKnLP9;^($0JQ0I!*x`CZU6uP07*qoM6N<$f~E4i6#xJL literal 0 HcmV?d00001 diff --git a/assets/images/copy.png b/assets/images/copy.png new file mode 100644 index 0000000000000000000000000000000000000000..41d18c74a0b99de8c18439415788a8445abda013 GIT binary patch literal 556 zcmV+{0@MA8P)wIWn%g(fD}TAL6*D#8O`?GbBv0O+|yK9n72 zK!^|pyc~oOf63lt=l1S@A2YkN2+VdOYiPhrLI{8dAfo+zmp=OGoKFsI00TS{6Nt$8 z?VNA0i)A2P1eYK|fE1=ijTsWuW&0s>11>Q3uYdr8*lLaxR4`)}U(g3rLjEbUsaj35 z=OabV)C5^bjLCs?(syd4S$|TT+8gTS6)`q78;2bwR4D|qrY2arJr+}yW+ux4YittTT$&%OA~tqv#>+ zH-l^wktkKzG^o@f3WH#B_DpuzgEg(B!Ph_@?`XvJ@E$h1{$;=8#|hFX%0rDcBD}|H z3C3KHdGWEuhqf1-`_Vrs%0o?>6$`Y-5AF!J+A)H4C^FI{2$#pD;I&3KAi&KyL6V|X z)Yy3>`sZEp0rThCh}#N!a_Hof{f2EGo0g~` zl!a)1xNCXN@+Ym3mf+jx_u<_KkH0b+harCfO;5Sc4D$MK)cyc(BT55Ktj1aZ0000< KMNUMnLSTY+8cK%% literal 0 HcmV?d00001 diff --git a/assets/images/filter.png b/assets/images/filter.png new file mode 100644 index 0000000000000000000000000000000000000000..dc47944c0fcc8451947f517f3008658528f606b7 GIT binary patch literal 504 zcmVNZ&QsDO$c2g?g z*KyhMXpH-C@<-6NTs8rlSJ=FeQZ^Qs27Wu)b~i$5^)^Py&J|aC8hU*pr4Z`_kK5k> zg2ITL@OV*AstKt9SO*&BhMmz*lt?N7%-%B|A1I-GfDZM&_r6B`ykO{E%qD1o+1g{a z?(wb?Dti6XlzPp3aZa)`F}^;;jv{5Dg?~kJBzC=C z`~AIL&-#_LL;|9nf-u8D)@tozAl^mL&>68=CAkRhhV>Y*siq*8Lb#K%4zxSKk|Gpy z7Pz}X9%y&H?r0@|RdfTm0i*jvkLddj;!YL62E)qnCL88A1g3{qUf#U*V)8x{`5e?=ENV;&2#L4EJYEvK_pKpiR#Wri>Q;dZoMan0w?soomXLjdXfXfa%&tP%2nn$wkf|NO)X0;Py zOM*NB0e-Mw@uyl~X{B1=Pm!bLdZLgB!r#A?K|_v!&XK#ZH_-w)+sVw#&b=J_|aNod!$+Lg0hP(3A~jAY&}4n?lNXiowBs zlClIB34KlPJ=wNNh$8$~h|+QWWmA`kL~81?Bn_0^EL;S}c5*sB$K}#g;Ehs%=B8L* zXNV%&YI>D$oNuS|b4_(MnvNjVIZ&R38BS9vRcD5rv(HzUxrh`sGgSUgwD73<_*$qxUa9_I3AHt669R92(^?p?syJtgz^V+Ej4xhr$Pqrmj@C zd)5S%zKm@%z~O!+ObXuj6_(UKy%_LU27|MBy1kA1V5dZ)H07*qoM6N<$g5!TbA^-pY literal 0 HcmV?d00001 diff --git a/assets/images/gaming.png b/assets/images/gaming.png new file mode 100644 index 0000000000000000000000000000000000000000..63be2322f58c3486142964d54de2637c3fbe8252 GIT binary patch literal 705 zcmV;y0zUnTP)xK~#7F?NmW> z5>XKC{=)#4S>Or65wMDsF2f1LDudZ+P7vG}VFNqRP4EQj7Gen}$hb7b%H#%fgxLv% z`EB=SW-`DCaixpAD&YTN{(GTWZZ=`> zpz|6KPkHIz%qEZBVcQr}(F>Vu{+NG?NUHL;1zFHID@JbrVO&%bvtD5N9HsU2CNZ%z2Nx5t@^7WK2 zH_%)Q1YUFin4I|X%>JNzt`J8*`X{|K0Se|AD9fyc!{VFGLrw&IBQTfTOE{r`8~{|N z0w^R2Qd}z{6w%AGW-N?a`Ct(Qwsjl9D#8Q3GCLQM@!(x$lj|guntK2ikiuQwx<)_; zP?WGT^F;)=EP6yKBncC1uq@@9bnE$}I0A*LAjz1()V~YKCQK$%pZh7~j8miw(sNOB zB(WG9rtFFBSyZld+FYA0 z6FQMEjVJ4eJXf5Q)I}mPzd+K*9n5US#aeUfn)M|ClM)_SgevS@eZ4cA_Wne^Bx2{P nW;`JJob%yD|KtnYK+Fpgfut6H(-Dmv9ogmoP`5oLvjMb25dH%1+rlSIRVa+7zyVDnQY?#&I#h2 zfQb+yA9oj3-JXeUk`N)FWXUr<^{-d20sgVz?-YvF*AXZvfwd987Mu}cLWuG4p;_;D z>w`ZEFnhmQMuX2;qdYnq5W&DdQpiCf{|GUFdfe+g>0dZNapBb)wp=EFgcU;8YWLpP zmL_$YZ8m=>fu%CmYizC6UQGK^zc|0wdMD0->TQR zJtx5ILhCtuTSPYKb)GGwc72E8;rGpHLt}o#-h+y^*Xj)k!j&%i>Q5wVQSku}b7;Sa z^DQ>;0h=EYR6#2{?*gSmHagMTB*x+f66|bsY85%ZRc*ah2#>g`x$O`2cp|`TwY7l= z1yZzxq^~1U*#ec_49*vx6UKrnOT#?{>K%8X-|N)ovH;QjW+7*Bcgk=8k};#i0Ij-i zj8H_RLQ*!=GGs;>5%n`vr9$e4AS;mPz^aOjl7DS$as4Xy_aJ{M<%E2+r=iwmvrZ1P3wiQ7aA(dIh2VhK;I6p4yX$Hk&^<@E2^9+NpZ~=+CjfDRM zS>577tHdq7?yC0XV90eR6BTOZdvAvOXE{~cw`>^r;b*oqYA>9!2Y*{!07Tm3VHL(d z1b)b1r_RTPj(wGgdZKz1DgoFZ`H|VGJ|8}mO|)p&&CyW|7Z5>`xYNfbi7Jgy&&-q+ zsk?j>S-{mS0Mam)fyyQ_ju+KTo;Q{H0%C!ppoObUP@&7z8ef(6Tm)tYH*%0KjMDcV zII`Wb*KE-u%lxAPyvEq(C38?achQ6p*-gxQ5R)-GoP;deAYHWJL{qo~Hy9bE-uowy z`DDdY1xEUa=1fk9M8*XRhJHRK2OkyS-d5+SqY}qu4n1l<0T`klsLJag%J4)a;B#ko+nuDL$;JOzU|{kk*=ZdVh%RwBDIlCR0Vk7tI?? zuS_O1R!>G&1sH8A$(70TOX>N-0aCfcOlwHr6;!dKaAY)KlInwn&9m;}|0X{HyC#$D Tc$VBx00000NkvXXu0mjftyIGc literal 0 HcmV?d00001 diff --git a/assets/images/mastercard.png b/assets/images/mastercard.png new file mode 100644 index 0000000000000000000000000000000000000000..2a80f52fe013a36cc2c7ac5204f7fbc97e0bd39f GIT binary patch literal 2114 zcmV-I2)*}-P)1^@s6d7R)b00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yPi%^;?wPrF z6h^=%ip3^!Wf#ts0Jfxn5@3r8s%U{?XFu9rv#>RKRi*8Z9jG+}|M4YkRFE)sxmc`Y zyyU>W?igD#3Lb6sEVWS`Y9qC)rrK>(*46@Qr_rWTKx0+S)oMr>g@ky9sWPBv=n(Xb zGKQx@ja_jNJetHt6*0gf?V<1?b^He;MCq_FgFv5OHNYdInk$95XRAnnSOsHCGSmSE z_5;KtQZGXBF5<~O48ZpCRN1x80+MZGl^%=ZYUe?mmKTd)ER>J@4RUce?U4Va5vkH2 zu-LC~^}ExvjsgqiVRTfX>ntXqjhJH{<_Vq6%bOkr86&_zq3iv8f-L9J~d!Pu%OPThe_ zs`O{gjW7EUU{$?Qb}={i)e<5SNidF80rIyg)t!Z4^4GVKC7bfEoP%Vn!krv;mZBsj zeG9gUDbI&-%J#A2L{UAUiZK{%co zbMA9yd)iK?3g%;i2brQ{7K@cHeShYD?s^^0`VJ(+<*|mDxP96_Jq7y?Z?IT5AsAio z`x2`|=Cc}9y#>)I%nXYY>de8Wey36iy+C^f^23p!wU^U|kKV@L3D( z!TGiU=_sY&@4OBLqs)-286Jcar*(DE-|Qjm-#2+8yCb0IZcA&3AIL>79 zg6Vn_(ov+9wy-br0?@wD+2q}oN`Zx0WlNkTdJv8-B;T28U%p^G+lK`cZ(+^|1VhV0 zIy}236@rZd<)^?Uwgvl{6!EiQDT4(|e46L51a${VW8LJB5d*bU2sRQjiOn944H|+~ zS+FXEV@6?NWAZJG()t>erpy-*j;Xsy8dJzZI=r%irgBFjv)$vQwVAwNe1WNWtf`V< zO1xmf^DJyL*%)IB8AvBE&Z>6dvS8v95C3^`F3Vhy?+%%BAFo&RB&sZye7CrfT&D&X zb4URd{t*tGDVRNga9ZDtAt_8Tx$lk(im)sE914~}W(C4&ZHy(7-gB~%iuko3JmTk2 zu(n&QKsb$0^N1V&%6X)KN@ptVp<1Gfyk^9x+54mrd$yP+cgP$Fb_wehE`2~SwyClI zWqb(&Pga*d{<3S?L(3fK|=SE}_C)5}Wuq(5H5sMnNQUg*;IWM_qj zsu>gWPv*|rukYMAt)l&u#A5osF7Y++7s`%lyQ}9>|KE=*khb0`N*Bw9Zm>_Wn8>5g zXSlpb$NB3ZpB)wMrRG}hFADa2Msmf*l3IVxfAl0$ju83EUrt1P1aAr*r}2!2hmB{> zd-j5Uk6*X93B#@hK7JEbEM|yx>%F26e}N8u12U{_la#qduKP$Q?A@K7g<_8&qCUzl z@NYR4Z6)o)4BgRRW6E>k{1Y#3IPfxr3p-&4J4;v5y%YA9r#xew1#&06mexyYBlrYy zpl+Qu#_EUI8c(sU+>UK!jHXjI=^+1ZA-MH=pLaVtCLEJ=J7W{7L+lM^i*vwt4ze?J zn#POy7}NoLl2!k!j{2Qm7IhXZ9E?+LyGpOUIj(lH#dEvvsX#r5OQ;Ts{)e&EQF8Kr zd*PN+#N&37h4y85>Ob^cc~LTpkNV%IOKwKZP|C*ZIh8S38~FuaD)=Y~kr1&_v9N|> zv48?ks=n^SOp^8eQk|Gzt-?F>(c?>cze*ccDTx^AHIpo$(%ORxN-njL@6cz}j}!Nm sRpA}_0);JO@z^n{I_}m-a(6!F2gMud)jP@6n*aa+07*qoM6N<$f&sS!@c;k- literal 0 HcmV?d00001 diff --git a/assets/images/mini_search_icon.png b/assets/images/mini_search_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d9d71a4d7d21a6043e4349c8087309a83eafea4a GIT binary patch literal 536 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 IM6N<$f_YfQ{Qv*} literal 0 HcmV?d00001 diff --git a/assets/images/tshirt.png b/assets/images/tshirt.png new file mode 100644 index 0000000000000000000000000000000000000000..cb20f60a49c01d7ba5e1f9a13bb3b4d385e02732 GIT binary patch literal 583 zcmV-N0=WH&P)*?Ih<8VlB=f&=Z zmpinaeXWz11mN)Jv!f@Ztn%Ngg>34CGg4URn9g88Wq`hK=$)e6o{96V%5^!FyAeGQ z((5!>Xy5acp||ga94PR12fc%K#pXchPwv@%wi}#@D>jD4PeUia#^mG*`D{+&yQL`0 z`f_M*B<~a-_E_f+hXbea;`k7Q$ngMBUCBoQWm_zZvu!@v-M5OJmlA`L9t7a=!P0tL z8W!s`apXS_RzNye%1fEH`sAvx1O!|Lyp(>xZvew7d~9-~$5TY)vaqi8&O_?7^uY}> zfjGsrNWo-ZC-jYOFJdJ>l0`t^aWgHRf+CEK)`dCt7CkO0pGQR V%fPaw->v`v002ovPDHLkV1k2F1m^$% literal 0 HcmV?d00001 diff --git a/assets/images/wifi.png b/assets/images/wifi.png new file mode 100644 index 0000000000000000000000000000000000000000..7834ef1c3b33c34621df701ec37266b77da7b73c GIT binary patch literal 1087 zcmV-F1i<@=P)O{ z-RXP>1ETOD2!RyQ2hJNa8aSAF>3=T#_3W?hY&J{jT;P@8@F|?X44nhKq6?1WKrP}E z4Ht7j&keJoslzKd_gBUkmZlD`!&R%DI<~ zztQ8c2iH+ri zxTYb1i%`uy<+zx)SAb9Q=1#ajlFMia2#UXj1-u8r2uNn3mFWzOL)&pAtSgj}5@G^m z`b~x`&+QG6k*2}|$SeVWzl9MONy-TdSs;TF+g2DSt2;)gU^xmBAhXtbl<_p!Qr}=X ztZjsUuHg?d=uNTJhRNGOS6qa6p1oHBpA@1f3KGD-1N>5D*nS@1a(|91jIh=`AmbOF zOc=uf8B1+LP{sm&DGF|t{Ae|JsC$2eT0KFQLn;~tzI}c4KRJIOKi;`U22@1Nv7$KLWa;n$UEGZ-fD(^}aXU9?0b3ZX)zO#qk-?Q3N1^ zl~CINVMpd3{CzEmYQkEp5OPTERY1EhfQk&{(Hwt~OoXNiXnb8lL}zXx<}@l}Gx0I~OgJE0u4gs03C;E&-bBv=#yh`%{P!(tnn7LJ9jC@$Jn z4m2xJi|awj-)&1w13?+)cvDnT<{FH#1pM#U6s-f64>O>K=mzjh{XledW=nm6t2ahW zM5f7fQMb`M0n3{rNSTesU_l0MjZvRqyxHj(dtYU+AeU%mIs_s4U7n42GxlIjaXV}W3RvpFV*4oVt9W*Xjq z0iPa~O)8XMPR9FMuv{sQ)B74!g#MbQ46opKLHR#4Ww4y}TMPXme>7!;|L!LL{UGBP zt7(V+Yj|aM{`&V?d2TuDkLA outputs; - BitcoinTransactionPriority priority; + final BitcoinTransactionPriority priority; + final int feeRate; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f6d3f30f8..a4db681f3 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -208,8 +208,14 @@ abstract class ElectrumWalletBase extends WalletBase minAmount) { @@ -346,45 +364,57 @@ abstract class ElectrumWalletBase extends WalletBase feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountWithFeeRate(int feeRate, int inputsCount, + int outputsCount) => + feeRate * estimatedTransactionSize(inputsCount, outputsCount); + @override int calculateEstimatedFee(TransactionPriority priority, int amount, {int outputsCount}) { if (priority is BitcoinTransactionPriority) { - int inputsCount = 0; - - if (amount != null) { - int totalValue = 0; - - for (final input in unspentCoins) { - if (totalValue >= amount) { - break; - } - - if (input.isSending) { - totalValue += input.value; - inputsCount += 1; - } - } - - if (totalValue < amount) return 0; - } else { - for (final input in unspentCoins) { - if (input.isSending) { - inputsCount += 1; - } - } - } - - // If send all, then we have no change value - final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); - - return feeAmountForPriority( - priority, inputsCount, _outputsCount); + return calculateEstimatedFeeWithFeeRate( + feeRate(priority), + amount, + outputsCount: outputsCount); } return 0; } + int calculateEstimatedFeeWithFeeRate(int feeRate, int amount, + {int outputsCount}) { + int inputsCount = 0; + + if (amount != null) { + int totalValue = 0; + + for (final input in unspentCoins) { + if (totalValue >= amount) { + break; + } + + if (input.isSending) { + totalValue += input.value; + inputsCount += 1; + } + } + + if (totalValue < amount) return 0; + } else { + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount += 1; + } + } + } + + // If send all, then we have no change value + final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); + + return feeAmountWithFeeRate( + feeRate, inputsCount, _outputsCount); + } + @override Future save() async { final path = await makePath(); @@ -525,10 +555,6 @@ abstract class ElectrumWalletBase extends WalletBase{}; final normalizedHistories = >[]; walletAddresses.addresses.forEach((addressRecord) { - if (addressRecord.isHidden) { - return; - } - final sh = scriptHash(addressRecord.address, networkType: networkType); addressHashes[sh] = addressRecord; }); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 1793db354..b9f754c72 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -24,6 +24,9 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get id => _tx.getId(); + @override + String get hex => _tx.toHex(); + @override String get amountFormatted => bitcoinAmountToString(amount: amount); diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index c7f9b77d5..cc5686fc9 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -2,6 +2,7 @@ mixin PendingTransaction { String get id; String get amountFormatted; String get feeFormatted; + String get hex; Future commit(); } \ No newline at end of file diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart index 7a8c6acc5..d56b5096c 100644 --- a/cw_haven/lib/pending_haven_transaction.dart +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -22,6 +22,9 @@ class PendingHavenTransaction with PendingTransaction { @override String get id => pendingTransactionDescription.hash; + @override + String get hex => ''; + @override String get amountFormatted => AmountConverter.amountIntToString( cryptoCurrency, pendingTransactionDescription.amount); diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index 37e6b72b7..f9b6c4890 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -169,13 +169,6 @@ packages: relative: true source: path version: "0.0.1" - cw_monero: - dependency: "direct main" - description: - path: "../cw_monero" - relative: true - source: path - version: "0.0.1" dart_style: dependency: transitive description: diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 4d3f5f56b..6bb251dcf 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -166,6 +166,8 @@ extern "C" uint64_t amount; uint64_t fee; char *hash; + char *hex; + char *txKey; Monero::PendingTransaction *transaction; PendingTransactionRaw(Monero::PendingTransaction *_transaction) @@ -174,6 +176,8 @@ extern "C" amount = _transaction->amount(); fee = _transaction->fee(); hash = strdup(_transaction->txid()[0].c_str()); + hex = strdup(_transaction->hex()[0].c_str()); + txKey = strdup(_transaction->txKey()[0].c_str()); } }; @@ -228,8 +232,6 @@ extern "C" bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) { - Monero::WalletManagerFactory::setLogLevel(4); - Monero::NetworkType _networkType = static_cast(networkType); Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index b492f28a0..edbd2d0ff 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -10,14 +10,30 @@ class PendingTransactionRaw extends Struct { Pointer hash; + Pointer hex; + + Pointer txKey; + String getHash() => Utf8.fromUtf8(hash); + + String getHex() => Utf8.fromUtf8(hex); + + String getKey() => Utf8.fromUtf8(txKey); } class PendingTransactionDescription { - PendingTransactionDescription({this.amount, this.fee, this.hash, this.pointerAddress}); + PendingTransactionDescription({ + this.amount, + this.fee, + this.hash, + this.hex, + this.txKey, + this.pointerAddress}); final int amount; final int fee; final String hash; + final String hex; + final String txKey; final int pointerAddress; } \ No newline at end of file diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index d693e16b9..9546a93d3 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -104,6 +104,8 @@ PendingTransactionDescription createTransactionSync( amount: pendingTransactionRawPointer.ref.amount, fee: pendingTransactionRawPointer.ref.fee, hash: pendingTransactionRawPointer.ref.getHash(), + hex: pendingTransactionRawPointer.ref.getHex(), + txKey: pendingTransactionRawPointer.ref.getKey(), pointerAddress: pendingTransactionRawPointer.address); } @@ -157,6 +159,8 @@ PendingTransactionDescription createTransactionMultDestSync( amount: pendingTransactionRawPointer.ref.amount, fee: pendingTransactionRawPointer.ref.fee, hash: pendingTransactionRawPointer.ref.getHash(), + hex: pendingTransactionRawPointer.ref.getHex(), + txKey: pendingTransactionRawPointer.ref.getKey(), pointerAddress: pendingTransactionRawPointer.address); } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index d927dd0d7..d32bab2ce 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -22,6 +22,11 @@ class PendingMoneroTransaction with PendingTransaction { @override String get id => pendingTransactionDescription.hash; + @override + String get hex => pendingTransactionDescription.hex; + + String get txKey => pendingTransactionDescription.txKey; + @override String get amountFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.amount); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d90ef8ca3..d527812b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,8 @@ PODS: - Flutter - cw_shared_external/Sodium (0.0.1): - Flutter + - device_display_brightness (0.0.1): + - Flutter - devicelocale (0.0.1): - Flutter - DKImagePickerController/Core (4.3.2): @@ -134,6 +136,7 @@ DEPENDENCIES: - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) + - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -174,6 +177,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_monero/ios" cw_shared_external: :path: ".symlinks/plugins/cw_shared_external/ios" + device_display_brightness: + :path: ".symlinks/plugins/device_display_brightness/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: @@ -211,6 +216,7 @@ SPEC CHECKSUMS: cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 diff --git a/lib/anypay/any_pay_chain.dart b/lib/anypay/any_pay_chain.dart new file mode 100644 index 000000000..3f6d9b135 --- /dev/null +++ b/lib/anypay/any_pay_chain.dart @@ -0,0 +1,5 @@ +class AnyPayChain { + static const xmr = 'XMR'; + static const btc = 'BTC'; + static const ltc = 'LTC'; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment.dart b/lib/anypay/any_pay_payment.dart new file mode 100644 index 000000000..295ee2dc4 --- /dev/null +++ b/lib/anypay/any_pay_payment.dart @@ -0,0 +1,64 @@ +import 'package:cake_wallet/anypay/any_pay_chain.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; + +class AnyPayPayment { + AnyPayPayment({ + @required this.time, + @required this.expires, + @required this.memo, + @required this.paymentUrl, + @required this.paymentId, + @required this.chain, + @required this.network, + @required this.instructions}); + + factory AnyPayPayment.fromMap(Map obj) { + final instructions = (obj['instructions'] as List) + .map((dynamic instruction) => AnyPayPaymentInstruction.fromMap(instruction as Map)) + .toList(); + return AnyPayPayment( + time: DateTime.parse(obj['time'] as String), + expires: DateTime.parse(obj['expires'] as String), + memo: obj['memo'] as String, + paymentUrl: obj['paymentUrl'] as String, + paymentId: obj['paymentId'] as String, + chain: obj['chain'] as String, + network: obj['network'] as String, + instructions: instructions); + } + + final DateTime time; + final DateTime expires; + final String memo; + final String paymentUrl; + final String paymentId; + final String chain; + final String network; + final List instructions; + + String get totalAmount { + final total = instructions + .fold(0, (int acc, instruction) => acc + instruction.outputs + .fold(0, (int outAcc, out) => outAcc + out.amount)); + switch (chain) { + case AnyPayChain.xmr: + return moneroAmountToString(amount: total); + case AnyPayChain.btc: + return bitcoinAmountToString(amount: total); + case AnyPayChain.ltc: + return bitcoinAmountToString(amount: total); + default: + return null; + } + } + + List get outAddresses { + return instructions + .map((instuction) => instuction.outputs.map((out) => out.address)) + .expand((e) => e) + .toList(); + } +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_committed_info.dart b/lib/anypay/any_pay_payment_committed_info.dart new file mode 100644 index 000000000..126b3d92e --- /dev/null +++ b/lib/anypay/any_pay_payment_committed_info.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; + +class AnyPayPaymentCommittedInfo { + const AnyPayPaymentCommittedInfo({ + @required this.uri, + @required this.currency, + @required this.chain, + @required this.transactions, + @required this.memo}); + + final String uri; + final String currency; + final String chain; + final List transactions; + final String memo; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_instruction.dart b/lib/anypay/any_pay_payment_instruction.dart new file mode 100644 index 000000000..41d2ee82d --- /dev/null +++ b/lib/anypay/any_pay_payment_instruction.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_instruction_output.dart'; + +class AnyPayPaymentInstruction { + AnyPayPaymentInstruction({ + @required this.type, + @required this.requiredFeeRate, + @required this.txKey, + @required this.txHash, + @required this.outputs}); + + factory AnyPayPaymentInstruction.fromMap(Map obj) { + final outputs = (obj['outputs'] as List) + .map((dynamic out) => + AnyPayPaymentInstructionOutput.fromMap(out as Map)) + .toList(); + return AnyPayPaymentInstruction( + type: obj['type'] as String, + requiredFeeRate: obj['requiredFeeRate'] as int, + txKey: obj['tx_key'] as bool, + txHash: obj['tx_hash'] as bool, + outputs: outputs); + } + + static const transactionType = 'transaction'; + + final String type; + final int requiredFeeRate; + final bool txKey; + final bool txHash; + final List outputs; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_instruction_output.dart b/lib/anypay/any_pay_payment_instruction_output.dart new file mode 100644 index 000000000..7fabea966 --- /dev/null +++ b/lib/anypay/any_pay_payment_instruction_output.dart @@ -0,0 +1,10 @@ +class AnyPayPaymentInstructionOutput { + const AnyPayPaymentInstructionOutput(this.address, this.amount); + + factory AnyPayPaymentInstructionOutput.fromMap(Map obj) { + return AnyPayPaymentInstructionOutput(obj['address'] as String, obj['amount'] as int); + } + + final String address; + final int amount; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_trasnaction.dart b/lib/anypay/any_pay_trasnaction.dart new file mode 100644 index 000000000..29f8a0152 --- /dev/null +++ b/lib/anypay/any_pay_trasnaction.dart @@ -0,0 +1,9 @@ +import 'package:flutter/foundation.dart'; + +class AnyPayTransaction { + const AnyPayTransaction(this.tx, {@required this.id, @required this.key}); + + final String tx; + final String id; + final String key; +} \ No newline at end of file diff --git a/lib/anypay/anypay_api.dart b/lib/anypay/anypay_api.dart new file mode 100644 index 000000000..c0727bc29 --- /dev/null +++ b/lib/anypay/anypay_api.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/anypay/any_pay_payment.dart'; +import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; + +class AnyPayApi { + static const contentTypePaymentRequest = 'application/payment-request'; + static const contentTypePayment = 'application/payment'; + static const xPayproVersion = '2'; + + static String chainByScheme(String scheme) { + switch (scheme.toLowerCase()) { + case 'monero': + return CryptoCurrency.xmr.title; + case 'bitcoin': + return CryptoCurrency.btc.title; + case 'litecoin': + return CryptoCurrency.ltc.title; + default: + return ''; + } + } + + static CryptoCurrency currencyByScheme(String scheme) { + switch (scheme.toLowerCase()) { + case 'monero': + return CryptoCurrency.xmr; + case 'bitcoin': + return CryptoCurrency.btc; + case 'litecoin': + return CryptoCurrency.ltc; + default: + return null; + } + } + + Future paymentRequest(String uri) async { + final fragments = uri.split(':?r='); + final scheme = fragments.first; + final url = fragments[1]; + final headers = { + 'Content-Type': contentTypePaymentRequest, + 'X-Paypro-Version': xPayproVersion, + 'Accept': '*/*',}; + final body = { + 'chain': chainByScheme(scheme), + 'currency': currencyByScheme(scheme).title}; + final response = await post(url, headers: headers, body: utf8.encode(json.encode(body))); + + if (response.statusCode != 200) { + return null; + } + + final decodedBody = json.decode(response.body) as Map; + return AnyPayPayment.fromMap(decodedBody); + } + + Future payment( + String uri, + {@required String chain, + @required String currency, + @required List transactions}) async { + final headers = { + 'Content-Type': contentTypePayment, + 'X-Paypro-Version': xPayproVersion, + 'Accept': '*/*',}; + final body = { + 'chain': chain, + 'currency': currency, + 'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()}; + final response = await post(uri, headers: headers, body: utf8.encode(json.encode(body))); + if (response.statusCode == 400) { + final decodedBody = json.decode(response.body) as Map; + throw Exception(decodedBody['message'] as String); + } + + if (response.statusCode != 200) { + throw Exception('Unexpected response'); + } + + final decodedBody = json.decode(response.body) as Map; + return AnyPayPaymentCommittedInfo( + uri: uri, + currency: currency, + chain: chain, + transactions: transactions, + memo: decodedBody['memo'] as String); + } +} \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e73861594..46ef89172 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -55,7 +55,7 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, TransactionPriority priority) + Object createBitcoinTransactionCredentials(List outputs, {TransactionPriority priority, int feeRate}) => BitcoinTransactionCredentials( outputs.map((out) => OutputInfo( fiatAmount: out.fiatAmount, @@ -67,7 +67,15 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority as BitcoinTransactionPriority); + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); + + @override + Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority priority, int feeRate}) + => BitcoinTransactionCredentials( + outputs, + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); @override List getAddresses(Object wallet) { diff --git a/lib/core/email_validator.dart b/lib/core/email_validator.dart new file mode 100644 index 000000000..23910db60 --- /dev/null +++ b/lib/core/email_validator.dart @@ -0,0 +1,11 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class EmailValidator extends TextValidator { + EmailValidator() + : super( + errorMessage: 'Invalid email address', + pattern: + '^[^@]+@[^@]+\.[^@]+', + ); +} diff --git a/lib/di.dart b/lib/di.dart index 2270f1863..bd911335d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,10 +1,27 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; +import 'package:cake_wallet/ionia/ionia_anypay.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_api.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -100,6 +117,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -123,6 +141,12 @@ import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; +import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -261,7 +285,6 @@ Future setup( fiatConvertationStore: getIt.get())); getIt.registerFactory(() => DashboardViewModel( - balanceViewModel: getIt.get(), appStore: getIt.get(), tradesStore: getIt.get(), @@ -560,10 +583,6 @@ Future setup( getIt.registerFactory(() => BackupPage(getIt.get())); - getIt.registerFactory(() => EditBackupPasswordViewModel( - getIt.get(), getIt.get()) - ..init()); - getIt.registerFactory( () => EditBackupPasswordPage(getIt.get())); @@ -596,10 +615,7 @@ Future setup( final url = args.first as String; final buyViewModel = args[1] as BuyViewModel; - return BuyWebViewPage( - buyViewModel: buyViewModel, - ordersStore: getIt.get(), - url: url); + return BuyWebViewPage(buyViewModel: buyViewModel, ordersStore: getIt.get(), url: url); }); getIt.registerFactoryParam((order, _) { @@ -649,6 +665,102 @@ Future setup( getIt.registerFactoryParam( (String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,)); + + getIt.registerFactory(() => IoniaApi()); + + getIt.registerFactory(() => AnyPayApi()); + + getIt.registerFactory( + () => IoniaService(getIt.get(), getIt.get())); + + getIt.registerFactory( + () => IoniaAnyPay( + getIt.get(), + getIt.get(), + getIt.get().wallet)); + + getIt.registerFactory(() => IoniaFilterViewModel()); + + getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); + + getIt.registerFactoryParam((double amount, merchant) { + return IoniaMerchPurchaseViewModel( + ioniaAnyPayService: getIt.get(), + amount: amount, + ioniaMerchant: merchant, + ); + }); + + getIt.registerFactoryParam((IoniaMerchant merchant, _) { + return IoniaBuyCardViewModel(ioniaMerchant: merchant); + }); + + getIt.registerFactory(() => IoniaAccountViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get())); + + getIt.registerFactory(() => IoniaLoginPage(getIt.get())); + + getIt.registerFactoryParam((List args, _) { + final email = args.first as String; + final isSignIn = args[1] as bool; + + return IoniaVerifyIoniaOtp(getIt.get(), email, isSignIn); + }); + + getIt.registerFactory(() => IoniaWelcomePage(getIt.get())); + + getIt.registerFactoryParam((List args, _) { + final merchant = args.first as IoniaMerchant; + + return IoniaBuyGiftCardPage(getIt.get(param1: merchant)); + }); + + getIt.registerFactoryParam((List args, _) { + final amount = args.first as double; + final merchant = args.last as IoniaMerchant; + return IoniaBuyGiftCardDetailPage(getIt.get(param1: amount, param2: merchant)); + }); + + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { + return IoniaGiftCardDetailsViewModel( + ioniaService: getIt.get(), + giftCard: giftCard); + }); + + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { + return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); + }); + + getIt.registerFactoryParam((List args, _) { + final amount = args.first as String; + final merchant = args.last as IoniaMerchant; + + return IoniaCustomTipPage(getIt.get(param1: amount, param2: merchant)); + }); + + getIt.registerFactory(() => IoniaManageCardsPage(getIt.get())); + + getIt.registerFactory(() => IoniaDebitCardPage(getIt.get())); + + getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get())); + + getIt.registerFactory(() => IoniaAccountPage(getIt.get())); + + getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get())); + + getIt.registerFactoryParam( + (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) + => IoniaPaymentStatusViewModel( + getIt.get(), + paymentInfo: paymentInfo, + committedInfo: committedInfo)); + + getIt.registerFactoryParam( + (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) + => IoniaPaymentStatusPage(getIt.get(param1: paymentInfo, param2: committedInfo))); _isSetupFinished = true; } diff --git a/lib/ionia/ionia_any_pay_payment_info.dart b/lib/ionia/ionia_any_pay_payment_info.dart new file mode 100644 index 000000000..6146a46fe --- /dev/null +++ b/lib/ionia/ionia_any_pay_payment_info.dart @@ -0,0 +1,9 @@ +import 'package:cake_wallet/anypay/any_pay_payment.dart'; +import 'package:cake_wallet/ionia/ionia_order.dart'; + +class IoniaAnyPayPaymentInfo { + const IoniaAnyPayPaymentInfo(this.ioniaOrder, this.anyPayPayment); + + final IoniaOrder ioniaOrder; + final AnyPayPayment anyPayPayment; +} diff --git a/lib/ionia/ionia_anypay.dart b/lib/ionia/ionia_anypay.dart new file mode 100644 index 000000000..b9b53498a --- /dev/null +++ b/lib/ionia/ionia_anypay.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/anypay/any_pay_payment.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/anypay/any_pay_chain.dart'; +import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; +import 'package:cake_wallet/ionia/ionia_order.dart'; + +class IoniaAnyPay { + IoniaAnyPay(this.ioniaService, this.anyPayApi, this.wallet); + + final IoniaService ioniaService; + final AnyPayApi anyPayApi; + final WalletBase wallet; + + Future purchase({ + @required String merchId, + @required double amount}) async { + final invoice = await ioniaService.purchaseGiftCard( + merchId: merchId, + amount: amount, + currency: wallet.currency.title.toUpperCase()); + final anypayPayment = await anyPayApi.paymentRequest(invoice.uri); + return IoniaAnyPayPaymentInfo(invoice, anypayPayment); + } + + Future commitInvoice(AnyPayPayment payment) async { + final transactionCredentials = payment.instructions + .where((instruction) => instruction.type == AnyPayPaymentInstruction.transactionType) + .map((AnyPayPaymentInstruction instruction) { + switch(payment.chain.toUpperCase()) { + case AnyPayChain.xmr: + return monero.createMoneroTransactionCreationCredentialsRaw( + outputs: instruction.outputs.map((out) => + OutputInfo( + isParsedAddress: false, + address: out.address, + cryptoAmount: moneroAmountToString(amount: out.amount), + sendAll: false)).toList(), + priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY + case AnyPayChain.btc: + return bitcoin.createBitcoinTransactionCredentialsRaw( + instruction.outputs.map((out) => + OutputInfo( + isParsedAddress: false, + address: out.address, + formattedCryptoAmount: out.amount, + sendAll: false)).toList(), + feeRate: instruction.requiredFeeRate); + case AnyPayChain.ltc: + return bitcoin.createBitcoinTransactionCredentialsRaw( + instruction.outputs.map((out) => + OutputInfo( + isParsedAddress: false, + address: out.address, + formattedCryptoAmount: out.amount, + sendAll: false)).toList(), + feeRate: instruction.requiredFeeRate); + default: + throw Exception('Incorrect transaction chain: ${payment.chain.toUpperCase()}'); + } + }); + final transactions = (await Future.wait(transactionCredentials + .map((Object credentials) async => await wallet.createTransaction(credentials)))) + .map((PendingTransaction pendingTransaction) { + switch (payment.chain.toUpperCase()){ + case AnyPayChain.xmr: + final ptx = monero.pendingTransactionInfo(pendingTransaction); + return AnyPayTransaction(ptx['hex'], id: ptx['id'], key: ptx['key']); + default: + return AnyPayTransaction(pendingTransaction.hex, id: pendingTransaction.id, key: null); + } + }) + .toList(); + + return await anyPayApi.payment( + payment.paymentUrl, + chain: payment.chain, + currency: payment.chain, + transactions: transactions); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart new file mode 100644 index 000000000..6784e2ba1 --- /dev/null +++ b/lib/ionia/ionia_api.dart @@ -0,0 +1,444 @@ +import 'dart:convert'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_order.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:cake_wallet/ionia/ionia_user_credentials.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; + +class IoniaApi { + static const baseUri = 'apistaging.ionia.io'; + static const pathPrefix = 'cake'; + static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser'); + static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail'); + static final signInUri = Uri.https(baseUri, '/$pathPrefix/SignIn'); + static final createCardUri = Uri.https(baseUri, '/$pathPrefix/CreateCard'); + static final getCardsUri = Uri.https(baseUri, '/$pathPrefix/GetCards'); + static final getMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchants'); + static final getMerchantsByFilterUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchantsByFilter'); + static final getPurchaseMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/PurchaseGiftCard'); + static final getCurrentUserGiftCardSummariesUrl = Uri.https(baseUri, '/$pathPrefix/GetCurrentUserGiftCardSummaries'); + static final changeGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/ChargeGiftCard'); + static final getGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/GetGiftCard'); + static final getPaymentStatusUrl = Uri.https(baseUri, '/$pathPrefix/PaymentStatus'); + + // Create user + + Future createUser(String email, {@required String clientId}) async { + final headers = {'clientId': clientId}; + final query = {'emailAddress': email}; + final uri = createUserUri.replace(queryParameters: query); + final response = await put(uri, headers: headers); + + if (response.statusCode != 200) { + // throw exception + return null; + } + + final bodyJson = json.decode(response.body) as Map; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['ErrorMessage'] as String); + } + + return data['username'] as String; + } + + // Verify email + + Future verifyEmail({ + @required String username, + @required String email, + @required String code, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'EmailAddress': email}; + final query = {'verificationCode': code}; + final uri = verifyEmailUri.replace(queryParameters: query); + final response = await put(uri, headers: headers); + + if (response.statusCode != 200) { + // throw exception + return null; + } + + final bodyJson = json.decode(response.body) as Map; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(bodyJson['ErrorMessage'] as String); + } + + final password = data['password'] as String; + username = data['username'] as String; + return IoniaUserCredentials(username, password); + } + + // Sign In + + Future signIn(String email, {@required String clientId}) async { + final headers = {'clientId': clientId}; + final query = {'emailAddress': email}; + final uri = signInUri.replace(queryParameters: query); + final response = await put(uri, headers: headers); + + if (response.statusCode != 200) { + // throw exception + return null; + } + + final bodyJson = json.decode(response.body) as Map; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['ErrorMessage'] as String); + } + + return data['username'] as String; + } + + // Get virtual card + + Future getCards({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password}; + final response = await post(getCardsUri, headers: headers); + + if (response.statusCode != 200) { + // throw exception + return null; + } + + final bodyJson = json.decode(response.body) as Map; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['message'] as String); + } + + final virtualCard = data['VirtualCard'] as Map; + return IoniaVirtualCard.fromMap(virtualCard); + } + + // Create virtual card + + Future createCard({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password}; + final response = await post(createCardUri, headers: headers); + + if (response.statusCode != 200) { + // throw exception + return null; + } + + final bodyJson = json.decode(response.body) as Map; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['message'] as String); + } + + return IoniaVirtualCard.fromMap(data); + } + + // Get Merchants + + Future> getMerchants({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password}; + final response = await post(getMerchantsUrl, headers: headers); + + if (response.statusCode != 200) { + return []; + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e as Map; + return IoniaMerchant.fromJsonMap(element); + } catch(_) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Get Merchants By Filter + + Future> getMerchantsByFilter({ + @required String username, + @required String password, + @required String clientId, + String search, + List categories, + int merchantFilterType = 0}) async { + // MerchantFilterType: {All = 0, Nearby = 1, Popular = 2, Online = 3, MyFaves = 4, Search = 5} + + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = {'MerchantFilterType': merchantFilterType}; + + if (search != null) { + body['SearchCriteria'] = search; + } + + if (categories != null) { + body['Categories'] = categories + .map((e) => e.ids) + .expand((e) => e) + .toList(); + } + + final response = await post(getMerchantsByFilterUrl, headers: headers, body: json.encode(body)); + + if (response.statusCode != 200) { + return []; + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e['Merchant'] as Map; + return IoniaMerchant.fromJsonMap(element); + } catch(_) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Purchase Gift Card + + Future purchaseGiftCard({ + @required String merchId, + @required double amount, + @required String currency, + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + 'Amount': amount, + 'Currency': currency, + 'MerchantId': merchId}; + final response = await post(getPurchaseMerchantsUrl, headers: headers, body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Unexpected response'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + throw Exception(decodedBody['ErrorMessage'] as String); + } + + final data = decodedBody['Data'] as Map; + return IoniaOrder.fromMap(data); + } + + // Get Current User Gift Card Summaries + + Future> getCurrentUserGiftCardSummaries({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password}; + final response = await post(getCurrentUserGiftCardSummariesUrl, headers: headers); + + if (response.statusCode != 200) { + return []; + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e as Map; + return IoniaGiftCard.fromJsonMap(element); + } catch(e) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Charge Gift Card + + Future chargeGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int giftCardId, + @required double amount}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + 'Id': giftCardId, + 'Amount': amount}; + final response = await post( + changeGiftCardUrl, + headers: headers, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Failed to update Gift Card with ID ${giftCardId};Incorrect response status: ${response.statusCode};'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final data = decodedBody['Data'] as Map; + final msg = data['Message'] as String ?? ''; + + if (msg.isNotEmpty) { + throw Exception(msg); + } + + throw Exception('Failed to update Gift Card with ID ${giftCardId};'); + } + } + + // Get Gift Card + + Future getGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int id}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = {'Id': id}; + final response = await post( + getGiftCardUrl, + headers: headers, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Failed to get Gift Card with ID ${id};Incorrect response status: ${response.statusCode};'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final msg = decodedBody['ErrorMessage'] as String ?? ''; + + if (msg.isNotEmpty) { + throw Exception(msg); + } + + throw Exception('Failed to get Gift Card with ID ${id};'); + } + + final data = decodedBody['Data'] as Map; + return IoniaGiftCard.fromJsonMap(data); + } + + // Payment Status + + Future getPaymentStatus({ + @required String username, + @required String password, + @required String clientId, + @required String orderId, + @required String paymentId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + 'order_id': orderId, + 'paymentId': paymentId}; + final response = await post( + getPaymentStatusUrl, + headers: headers, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId};Incorrect response status: ${response.statusCode};'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final msg = decodedBody['ErrorMessage'] as String ?? ''; + + if (msg.isNotEmpty) { + throw Exception(msg); + } + + throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId}'); + } + + final data = decodedBody['Data'] as Map; + return data['gift_card_id'] as int; + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_category.dart b/lib/ionia/ionia_category.dart new file mode 100644 index 000000000..4b94d70cf --- /dev/null +++ b/lib/ionia/ionia_category.dart @@ -0,0 +1,18 @@ +class IoniaCategory { + const IoniaCategory({this.index, this.title, this.ids, this.iconPath}); + + static const allCategories = [all, apparel, onlineOnly, food, entertainment, delivery, travel]; + static const all = IoniaCategory(index: 0, title: 'All', ids: [], iconPath: 'assets/images/category.png'); + static const apparel = IoniaCategory(index: 1, title: 'Apparel', ids: [1], iconPath: 'assets/images/tshirt.png'); + static const onlineOnly = IoniaCategory(index: 2, title: 'Online Only', ids: [13, 43], iconPath: 'assets/images/global.png'); + static const food = IoniaCategory(index: 3, title: 'Food', ids: [4], iconPath: 'assets/images/food.png'); + static const entertainment = IoniaCategory(index: 4, title: 'Entertainment', ids: [5], iconPath: 'assets/images/gaming.png'); + static const delivery = IoniaCategory(index: 5, title: 'Delivery', ids: [114, 109], iconPath: 'assets/images/delivery.png'); + static const travel = IoniaCategory(index: 6, title: 'Travel', ids: [12], iconPath: 'assets/images/airplane.png'); + + + final int index; + final String title; + final List ids; + final String iconPath; +} diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart new file mode 100644 index 000000000..b0277be45 --- /dev/null +++ b/lib/ionia/ionia_create_state.dart @@ -0,0 +1,58 @@ +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:flutter/material.dart'; + +abstract class IoniaCreateAccountState {} + +class IoniaInitialCreateState extends IoniaCreateAccountState {} + +class IoniaCreateStateSuccess extends IoniaCreateAccountState {} + +class IoniaCreateStateLoading extends IoniaCreateAccountState {} + +class IoniaCreateStateFailure extends IoniaCreateAccountState { + IoniaCreateStateFailure({@required this.error}); + + final String error; +} + +abstract class IoniaOtpState {} + +class IoniaOtpValidating extends IoniaOtpState {} + +class IoniaOtpSuccess extends IoniaOtpState {} + +class IoniaOtpSendDisabled extends IoniaOtpState {} + +class IoniaOtpSendEnabled extends IoniaOtpState {} + +class IoniaOtpFailure extends IoniaOtpState { + IoniaOtpFailure({@required this.error}); + + final String error; +} + +class IoniaCreateCardState {} + +class IoniaCreateCardSuccess extends IoniaCreateCardState {} + +class IoniaCreateCardLoading extends IoniaCreateCardState {} + +class IoniaCreateCardFailure extends IoniaCreateCardState { + IoniaCreateCardFailure({@required this.error}); + + final String error; +} + +class IoniaFetchCardState {} + +class IoniaNoCardState extends IoniaFetchCardState {} + +class IoniaFetchingCard extends IoniaFetchCardState {} + +class IoniaFetchCardFailure extends IoniaFetchCardState {} + +class IoniaCardSuccess extends IoniaFetchCardState { + IoniaCardSuccess({@required this.card}); + + final IoniaVirtualCard card; +} diff --git a/lib/ionia/ionia_gift_card.dart b/lib/ionia/ionia_gift_card.dart new file mode 100644 index 000000000..ebe8084f8 --- /dev/null +++ b/lib/ionia/ionia_gift_card.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart'; +import 'package:flutter/foundation.dart'; + +class IoniaGiftCard { + IoniaGiftCard({ + @required this.id, + @required this.merchantId, + @required this.legalName, + @required this.systemName, + @required this.barcodeUrl, + @required this.cardNumber, + @required this.cardPin, + @required this.instructions, + @required this.tip, + @required this.purchaseAmount, + @required this.actualAmount, + @required this.totalTransactionAmount, + @required this.totalDashTransactionAmount, + @required this.remainingAmount, + @required this.createdDateFormatted, + @required this.lastTransactionDateFormatted, + @required this.isActive, + @required this.isEmpty, + @required this.logoUrl}); + + factory IoniaGiftCard.fromJsonMap(Map element) { + return IoniaGiftCard( + id: element['Id'] as int, + merchantId: element['MerchantId'] as int, + legalName: element['LegalName'] as String, + systemName: element['SystemName'] as String, + barcodeUrl: element['BarcodeUrl'] as String, + cardNumber: element['CardNumber'] as String, + cardPin: element['CardPin'] as String, + tip: element['Tip'] as double, + purchaseAmount: element['PurchaseAmount'] as double, + actualAmount: element['ActualAmount'] as double, + totalTransactionAmount: element['TotalTransactionAmount'] as double, + totalDashTransactionAmount: element['TotalDashTransactionAmount'] as double, + remainingAmount: element['RemainingAmount'] as double, + isActive: element['IsActive'] as bool, + isEmpty: element['IsEmpty'] as bool, + logoUrl: element['LogoUrl'] as String, + createdDateFormatted: element['CreatedDate'] as String, + lastTransactionDateFormatted: element['LastTransactionDate'] as String, + instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String)); + } + + final int id; + final int merchantId; + final String legalName; + final String systemName; + final String barcodeUrl; + final String cardNumber; + final String cardPin; + final List instructions; + final double tip; + final double purchaseAmount; + final double actualAmount; + final double totalTransactionAmount; + final double totalDashTransactionAmount; + final double remainingAmount; + final String createdDateFormatted; + final String lastTransactionDateFormatted; + final bool isActive; + final bool isEmpty; + final String logoUrl; +} \ No newline at end of file diff --git a/lib/ionia/ionia_gift_card_instruction.dart b/lib/ionia/ionia_gift_card_instruction.dart new file mode 100644 index 000000000..da1fdae1b --- /dev/null +++ b/lib/ionia/ionia_gift_card_instruction.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; +import 'package:intl/intl.dart' show toBeginningOfSentenceCase; + +class IoniaGiftCardInstruction { + IoniaGiftCardInstruction(this.header, this.body); + + factory IoniaGiftCardInstruction.fromJsonMap(Map element) { + return IoniaGiftCardInstruction( + toBeginningOfSentenceCase(element['title'] as String ?? ''), + element['description'] as String); + } + + static List parseListOfInstructions(String instructionsJSON) { + List instructions = []; + + if (instructionsJSON.isNotEmpty) { + final decodedInstructions = json.decode(instructionsJSON) as List; + instructions = decodedInstructions + .map((dynamic e) =>IoniaGiftCardInstruction.fromJsonMap(e as Map)) + .toList(); + } + + return instructions; + } + + final String header; + final String body; +} \ No newline at end of file diff --git a/lib/ionia/ionia_merchant.dart b/lib/ionia/ionia_merchant.dart new file mode 100644 index 000000000..16df62da7 --- /dev/null +++ b/lib/ionia/ionia_merchant.dart @@ -0,0 +1,176 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart'; + +class IoniaMerchant { + IoniaMerchant({ + @required this.id, + @required this.legalName, + @required this.systemName, + @required this.description, + @required this.website, + @required this.termsAndConditions, + @required this.logoUrl, + @required this.cardImageUrl, + @required this.cardholderAgreement, + @required this.purchaseFee, + @required this.revenueShare, + @required this.marketingFee, + @required this.minimumDiscount, + @required this.level1, + @required this.level2, + @required this.level3, + @required this.level4, + @required this.level5, + @required this.level6, + @required this.level7, + @required this.isActive, + @required this.isDeleted, + @required this.isOnline, + @required this.isPhysical, + @required this.isVariablePurchase, + @required this.minimumCardPurchase, + @required this.maximumCardPurchase, + @required this.acceptsTips, + @required this.createdDateFormatted, + @required this.createdBy, + @required this.isRegional, + @required this.modifiedDateFormatted, + @required this.modifiedBy, + @required this.usageInstructions, + @required this.usageInstructionsBak, + @required this.paymentGatewayId, + @required this.giftCardGatewayId, + @required this.isHtmlDescription, + @required this.purchaseInstructions, + @required this.balanceInstructions, + @required this.amountPerCard, + @required this.processingMessage, + @required this.hasBarcode, + @required this.hasInventory, + @required this.isVoidable, + @required this.receiptMessage, + @required this.cssBorderCode, + @required this.instructions, + @required this.alderSku, + @required this.ngcSku, + @required this.acceptedCurrency, + @required this.deepLink, + @required this.isPayLater, + @required this.savingsPercentage}); + + factory IoniaMerchant.fromJsonMap(Map element) { + return IoniaMerchant( + id: element["Id"] as int, + legalName: element["LegalName"] as String, + systemName: element["SystemName"] as String, + description: element["Description"] as String, + website: element["Website"] as String, + termsAndConditions: element["TermsAndConditions"] as String, + logoUrl: element["LogoUrl"] as String, + cardImageUrl: element["CardImageUrl"] as String, + cardholderAgreement: element["CardholderAgreement"] as String, + purchaseFee: element["PurchaseFee"] as double, + revenueShare: element["RevenueShare"] as double, + marketingFee: element["MarketingFee"] as double, + minimumDiscount: element["MinimumDiscount"] as double, + level1: element["Level1"] as double, + level2: element["Level2"] as double, + level3: element["Level3"] as double, + level4: element["Level4"] as double, + level5: element["Level5"] as double, + level6: element["Level6"] as double, + level7: element["Level7"] as double, + isActive: element["IsActive"] as bool, + isDeleted: element["IsDeleted"] as bool, + isOnline: element["IsOnline"] as bool, + isPhysical: element["IsPhysical"] as bool, + isVariablePurchase: element["IsVariablePurchase"] as bool, + minimumCardPurchase: element["MinimumCardPurchase"] as double, + maximumCardPurchase: element["MaximumCardPurchase"] as double, + acceptsTips: element["AcceptsTips"] as bool, + createdDateFormatted: element["CreatedDate"] as String, + createdBy: element["CreatedBy"] as int, + isRegional: element["IsRegional"] as bool, + modifiedDateFormatted: element["ModifiedDate"] as String, + modifiedBy: element["ModifiedBy"] as int, + usageInstructions: element["UsageInstructions"] as String, + usageInstructionsBak: element["UsageInstructionsBak"] as String, + paymentGatewayId: element["PaymentGatewayId"] as int, + giftCardGatewayId: element["GiftCardGatewayId"] as int , + isHtmlDescription: element["IsHtmlDescription"] as bool, + purchaseInstructions: element["PurchaseInstructions"] as String, + balanceInstructions: element["BalanceInstructions"] as String, + amountPerCard: element["AmountPerCard"] as double, + processingMessage: element["ProcessingMessage"] as String, + hasBarcode: element["HasBarcode"] as bool, + hasInventory: element["HasInventory"] as bool, + isVoidable: element["IsVoidable"] as bool, + receiptMessage: element["ReceiptMessage"] as String, + cssBorderCode: element["CssBorderCode"] as String, + instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String), + alderSku: element["AlderSku"] as String, + ngcSku: element["NgcSku"] as String, + acceptedCurrency: element["AcceptedCurrency"] as String, + deepLink: element["DeepLink"] as String, + isPayLater: element["IsPayLater"] as bool, + savingsPercentage: element["SavingsPercentage"] as double); + } + + final int id; + final String legalName; + final String systemName; + final String description; + final String website; + final String termsAndConditions; + final String logoUrl; + final String cardImageUrl; + final String cardholderAgreement; + final double purchaseFee; + final double revenueShare; + final double marketingFee; + final double minimumDiscount; + final double level1; + final double level2; + final double level3; + final double level4; + final double level5; + final double level6; + final double level7; + final bool isActive; + final bool isDeleted; + final bool isOnline; + final bool isPhysical; + final bool isVariablePurchase; + final double minimumCardPurchase; + final double maximumCardPurchase; + final bool acceptsTips; + final String createdDateFormatted; + final int createdBy; + final bool isRegional; + final String modifiedDateFormatted; + final int modifiedBy; + final String usageInstructions; + final String usageInstructionsBak; + final int paymentGatewayId; + final int giftCardGatewayId; + final bool isHtmlDescription; + final String purchaseInstructions; + final String balanceInstructions; + final double amountPerCard; + final String processingMessage; + final bool hasBarcode; + final bool hasInventory; + final bool isVoidable; + final String receiptMessage; + final String cssBorderCode; + final List instructions; + final String alderSku; + final String ngcSku; + final String acceptedCurrency; + final String deepLink; + final bool isPayLater; + final double savingsPercentage; + + double get discount => savingsPercentage; + +} diff --git a/lib/ionia/ionia_order.dart b/lib/ionia/ionia_order.dart new file mode 100644 index 000000000..f9c35ea70 --- /dev/null +++ b/lib/ionia/ionia_order.dart @@ -0,0 +1,23 @@ +import 'package:flutter/foundation.dart'; + +class IoniaOrder { + IoniaOrder({@required this.id, + @required this.uri, + @required this.currency, + @required this.amount, + @required this.paymentId}); + factory IoniaOrder.fromMap(Map obj) { + return IoniaOrder( + id: obj['order_id'] as String, + uri: obj['uri'] as String, + currency: obj['currency'] as String, + amount: obj['amount'] as double, + paymentId: obj['paymentId'] as String); + } + + final String id; + final String uri; + final String currency; + final double amount; + final String paymentId; +} \ No newline at end of file diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart new file mode 100644 index 000000000..d1a2052f0 --- /dev/null +++ b/lib/ionia/ionia_service.dart @@ -0,0 +1,172 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_order.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/ionia/ionia_api.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; + +class IoniaService { + IoniaService(this.secureStorage, this.ioniaApi); + + static const ioniaEmailStorageKey = 'ionia_email'; + static const ioniaUsernameStorageKey = 'ionia_username'; + static const ioniaPasswordStorageKey = 'ionia_password'; + + static String get clientId => secrets.ioniaClientId; + + final FlutterSecureStorage secureStorage; + final IoniaApi ioniaApi; + + // Create user + + Future createUser(String email) async { + final username = await ioniaApi.createUser(email, clientId: clientId); + await secureStorage.write(key: ioniaEmailStorageKey, value: email); + await secureStorage.write(key: ioniaUsernameStorageKey, value: username); + } + + // Verify email + + Future verifyEmail(String code) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final email = await secureStorage.read(key: ioniaEmailStorageKey); + final credentials = await ioniaApi.verifyEmail(email: email, username: username, code: code, clientId: clientId); + await secureStorage.write(key: ioniaPasswordStorageKey, value: credentials.password); + await secureStorage.write(key: ioniaUsernameStorageKey, value: credentials.username); + } + + // Sign In + + Future signIn(String email) async { + final username = await ioniaApi.signIn(email, clientId: clientId); + await secureStorage.write(key: ioniaEmailStorageKey, value: email); + await secureStorage.write(key: ioniaUsernameStorageKey, value: username); + } + + Future getUserEmail() async { + return secureStorage.read(key: ioniaEmailStorageKey); + } + + // Check is user logined + + Future isLogined() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey) ?? ''; + final password = await secureStorage.read(key: ioniaPasswordStorageKey) ?? ''; + return username.isNotEmpty && password.isNotEmpty; + } + + // Logout + + Future logout() async { + await secureStorage.delete(key: ioniaUsernameStorageKey); + await secureStorage.delete(key: ioniaPasswordStorageKey); + } + + // Create virtual card + + Future createCard() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.createCard(username: username, password: password, clientId: clientId); + } + + // Get virtual card + + Future getCard() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getCards(username: username, password: password, clientId: clientId); + } + + // Get Merchants + + Future> getMerchants() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getMerchants(username: username, password: password, clientId: clientId); + } + + // Get Merchants By Filter + + Future> getMerchantsByFilter({ + String search, + List categories, + int merchantFilterType = 0}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getMerchantsByFilter( + username: username, + password: password, + clientId: clientId, + search: search, + categories: categories, + merchantFilterType: merchantFilterType); + } + + // Purchase Gift Card + + Future purchaseGiftCard({ + @required String merchId, + @required double amount, + @required String currency}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.purchaseGiftCard( + merchId: merchId, + amount: amount, + currency: currency, + username: username, + password: password, + clientId: clientId); + } + + // Get Current User Gift Card Summaries + + Future> getCurrentUserGiftCardSummaries() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getCurrentUserGiftCardSummaries(username: username, password: password, clientId: clientId); + } + + // Charge Gift Card + + Future chargeGiftCard({ + @required int giftCardId, + @required double amount}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + await ioniaApi.chargeGiftCard( + username: username, + password: password, + clientId: clientId, + giftCardId: giftCardId, + amount: amount); + } + + // Redeem + + Future redeem(IoniaGiftCard giftCard) async { + await chargeGiftCard(giftCardId: giftCard.id, amount: giftCard.remainingAmount); + } + + // Get Gift Card + + Future getGiftCard({@required int id}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getGiftCard(username: username, password: password, clientId: clientId,id: id); + } + + // Payment Status + + Future getPaymentStatus({ + @required String orderId, + @required String paymentId}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getPaymentStatus(username: username, password: password, clientId: clientId, orderId: orderId, paymentId: paymentId); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_tip.dart b/lib/ionia/ionia_tip.dart new file mode 100644 index 000000000..340c6226f --- /dev/null +++ b/lib/ionia/ionia_tip.dart @@ -0,0 +1,12 @@ +class IoniaTip { + const IoniaTip({this.originalAmount, this.percentage}); + final double originalAmount; + final double percentage; + double get additionalAmount => double.parse((originalAmount * percentage / 100).toStringAsFixed(2)); + + static const tipList = [ + IoniaTip(originalAmount: 0, percentage: 0), + IoniaTip(originalAmount: 10, percentage: 10), + IoniaTip(originalAmount: 20, percentage: 20) + ]; +} diff --git a/lib/ionia/ionia_token_data.dart b/lib/ionia/ionia_token_data.dart new file mode 100644 index 000000000..1baa4c63d --- /dev/null +++ b/lib/ionia/ionia_token_data.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; +import 'dart:convert'; + +class IoniaTokenData { + IoniaTokenData({@required this.accessToken, @required this.tokenType, @required this.expiredAt}); + + factory IoniaTokenData.fromJson(String source) { + final decoded = json.decode(source) as Map; + final accessToken = decoded['access_token'] as String; + final expiresIn = decoded['expires_in'] as int; + final tokenType = decoded['token_type'] as String; + final expiredAtInMilliseconds = decoded['expired_at'] as int; + DateTime expiredAt; + + if (expiredAtInMilliseconds != null) { + expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtInMilliseconds); + } else { + expiredAt = DateTime.now().add(Duration(seconds: expiresIn)); + } + + return IoniaTokenData( + accessToken: accessToken, + tokenType: tokenType, + expiredAt: expiredAt); + } + + final String accessToken; + final String tokenType; + final DateTime expiredAt; + + bool get isExpired => DateTime.now().isAfter(expiredAt); + + @override + String toString() => '$tokenType $accessToken'; + + String toJson() { + return json.encode({ + 'access_token': accessToken, + 'token_type': tokenType, + 'expired_at': expiredAt.millisecondsSinceEpoch + }); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_user_credentials.dart b/lib/ionia/ionia_user_credentials.dart new file mode 100644 index 000000000..c398385f5 --- /dev/null +++ b/lib/ionia/ionia_user_credentials.dart @@ -0,0 +1,6 @@ +class IoniaUserCredentials { + const IoniaUserCredentials(this.username, this.password); + + final String username; + final String password; +} \ No newline at end of file diff --git a/lib/ionia/ionia_virtual_card.dart b/lib/ionia/ionia_virtual_card.dart new file mode 100644 index 000000000..43cb07584 --- /dev/null +++ b/lib/ionia/ionia_virtual_card.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; + +class IoniaVirtualCard { + IoniaVirtualCard({ + @required this.token, + @required this.createdAt, + @required this.lastFour, + @required this.state, + @required this.pan, + @required this.cvv, + @required this.expirationMonth, + @required this.expirationYear, + @required this.fundsLimit, + @required this.spendLimit}); + + factory IoniaVirtualCard.fromMap(Map source) { + final created = source['created'] as String; + final createdAt = DateTime.tryParse(created); + + return IoniaVirtualCard( + token: source['token'] as String, + createdAt: createdAt, + lastFour: source['lastFour'] as String, + state: source['state'] as String, + pan: source['pan'] as String, + cvv: source['cvv'] as String, + expirationMonth: source['expirationMonth'] as String, + expirationYear: source['expirationYear'] as String, + fundsLimit: source['FundsLimit'] as double, + spendLimit: source['spend_limit'] as double); + } + + final String token; + final String lastFour; + final String state; + final String pan; + final String cvv; + final String expirationMonth; + final String expirationYear; + final DateTime createdAt; + final double fundsLimit; + final double spendLimit; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b9f8074e7..4d0ea6208 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -174,7 +176,8 @@ Future initialSetup( exchangeTemplates: exchangeTemplates, transactionDescriptionBox: transactionDescriptions, ordersSource: ordersSource, - unspentCoinsInfoSource: unspentCoinsInfoSource); + unspentCoinsInfoSource: unspentCoinsInfoSource, + ); await bootstrap(navigatorKey); monero?.onStartup(); } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 98ba26446..dfa308b0d 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -2,7 +2,7 @@ part of 'monero.dart'; class CWMoneroAccountList extends MoneroAccountList { CWMoneroAccountList(this._wallet); - Object _wallet; + final Object _wallet; @override @computed @@ -39,13 +39,13 @@ class CWMoneroAccountList extends MoneroAccountList { @override Future addAccount(Object wallet, {String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList.addAccount(label: label); + await moneroWallet.walletAddresses.accountList.addAccount(label: label); } @override Future setLabelAccount(Object wallet, {int accountIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList + await moneroWallet.walletAddresses.accountList .setLabelAccount( accountIndex: accountIndex, label: label); @@ -95,7 +95,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { @override Future addSubaddress(Object wallet, {int accountIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList + await moneroWallet.walletAddresses.subaddressList .addSubaddress( accountIndex: accountIndex, label: label); @@ -105,7 +105,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future setLabelSubaddress(Object wallet, {int accountIndex, int addressIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList + await moneroWallet.walletAddresses.subaddressList .setLabelSubaddress( accountIndex: accountIndex, addressIndex: addressIndex, @@ -140,35 +140,43 @@ class CWMonero extends Monero { return CWMoneroAccountList(wallet); } + @override MoneroSubaddressList getSubaddressList(Object wallet) { return CWMoneroSubaddressList(wallet); } + @override TransactionHistoryBase getTransactionHistory(Object wallet) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.transactionHistory; } + @override MoneroWalletDetails getMoneroWalletDetails(Object wallet) { return CWMoneroWalletDetails(wallet); } + @override int getHeigthByDate({DateTime date}) { return getMoneroHeigthByDate(date: date); } + @override TransactionPriority getDefaultTransactionPriority() { return MoneroTransactionPriority.slow; } + @override TransactionPriority deserializeMoneroTransactionPriority({int raw}) { return MoneroTransactionPriority.deserialize(raw: raw); } + @override List getTransactionPriorities() { return MoneroTransactionPriority.all; } + @override List getMoneroWordList(String language) { switch (language.toLowerCase()) { case 'english': @@ -196,14 +204,15 @@ class CWMonero extends Monero { } } + @override WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ String name, - String spendKey, - String viewKey, - String address, - String password, - String language, - int height}) { + String spendKey, + String viewKey, + String address, + String password, + String language, + int height}) { return MoneroRestoreWalletFromKeysCredentials( name: name, spendKey: spendKey, @@ -214,6 +223,7 @@ class CWMonero extends Monero { height: height); } + @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) { return MoneroRestoreWalletFromSeedCredentials( name: name, @@ -222,6 +232,7 @@ class CWMonero extends Monero { mnemonic: mnemonic); } + @override WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}) { return MoneroNewWalletCredentials( name: name, @@ -229,6 +240,7 @@ class CWMonero extends Monero { language: language); } + @override Map getKeys(Object wallet) { final moneroWallet = wallet as MoneroWallet; final keys = moneroWallet.keys; @@ -239,6 +251,7 @@ class CWMonero extends Monero { 'publicViewKey': keys.publicViewKey}; } + @override Object createMoneroTransactionCreationCredentials({List outputs, TransactionPriority priority}) { return MoneroTransactionCreationCredentials( outputs: outputs.map((out) => OutputInfo( @@ -254,49 +267,72 @@ class CWMonero extends Monero { priority: priority as MoneroTransactionPriority); } + @override + Object createMoneroTransactionCreationCredentialsRaw({List outputs, TransactionPriority priority}) { + return MoneroTransactionCreationCredentials( + outputs: outputs, + priority: priority as MoneroTransactionPriority); + } + + @override String formatterMoneroAmountToString({int amount}) { return moneroAmountToString(amount: amount); } + @override double formatterMoneroAmountToDouble({int amount}) { return moneroAmountToDouble(amount: amount); } + @override int formatterMoneroParseAmount({String amount}) { return moneroParseAmount(amount: amount); } + @override Account getCurrentAccount(Object wallet) { final moneroWallet = wallet as MoneroWallet; final acc = moneroWallet.walletAddresses.account; return Account(id: acc.id, label: acc.label); } + @override void setCurrentAccount(Object wallet, int id, String label) { final moneroWallet = wallet as MoneroWallet; moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label); } + @override void onStartup() { monero_wallet_api.onStartup(); } + @override int getTransactionInfoAccountId(TransactionInfo tx) { final moneroTransactionInfo = tx as MoneroTransactionInfo; return moneroTransactionInfo.accountIndex; } + @override WalletService createMoneroWalletService(Box walletInfoSource) { return MoneroWalletService(walletInfoSource); } + @override String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.getTransactionAddress(accountIndex, addressIndex); } + @override String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.getSubaddressLabel(accountIndex, addressIndex); } + + @override + Map pendingTransactionInfo(Object transaction) { + final ptx = transaction as PendingMoneroTransaction; + return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; + } } diff --git a/lib/router.dart b/lib/router.dart index a3c7573ea..1a9c3c2d5 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,6 +4,10 @@ import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; @@ -63,6 +67,10 @@ import 'package:flutter/services.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; RouteSettings currentRouteSettings; @@ -401,6 +409,58 @@ Route createRoute(RouteSettings settings) { param2: args['isLight'] as bool, )); + case Routes.ioniaWelcomePage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaLoginPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaCreateAccountPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaManageCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaBuyGiftCardPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.ioniaBuyGiftCardDetailPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.ioniaVerifyIoniaOtpPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) =>getIt.get(param1: args)); + + case Routes.ioniaDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaActivateDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaAccountPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaAccountCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaCustomTipPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) =>getIt.get(param1: args)); + + case Routes.ioniaGiftCardDetailPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args.first)); + + case Routes.ioniaPaymentStatusPage: + final args = settings.arguments as List; + final paymentInfo = args.first as IoniaAnyPayPaymentInfo; + final commitedInfo = args[1] as AnyPayPaymentCommittedInfo; + return CupertinoPageRoute(builder: (_) => getIt.get( + param1: paymentInfo, + param2: commitedInfo)); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 1200d558a..82bd46691 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -60,4 +60,18 @@ class Routes { static const moneroNewWalletFromWelcome = '/monero_new_wallet'; static const addressPage = '/address_page'; static const fullscreenQR = '/fullscreen_qr'; -} \ No newline at end of file + static const ioniaWelcomePage = '/cake_pay_welcome_page'; + static const ioniaCreateAccountPage = '/cake_pay_create_account_page'; + static const ioniaLoginPage = '/cake_pay_login_page'; + static const ioniaManageCardsPage = '/manage_cards_page'; + static const ioniaBuyGiftCardPage = '/buy_gift_card_page'; + static const ioniaBuyGiftCardDetailPage = '/buy_gift_card_detail_page'; + static const ioniaVerifyIoniaOtpPage = '/cake_pay_verify_otp_page'; + static const ioniaDebitCardPage = '/debit_card_page'; + static const ioniaActivateDebitCardPage = '/activate_debit_card_page'; + static const ioniaAccountPage = 'ionia_account_page'; + static const ioniaAccountCardsPage = 'ionia_account_cards_page'; + static const ioniaCustomTipPage = 'ionia_custom_tip_page'; + static const ioniaGiftCardDetailPage = '/ionia_gift_card_detail_page'; + static const ioniaPaymentStatusPage = '/ionia_payment_status_page'; +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index eb8c9ab17..6ed07850f 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/yat/yat_popup.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -14,19 +14,15 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/router.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; class DashboardPage extends BasePage { DashboardPage({ @@ -85,7 +81,7 @@ class DashboardPage extends BasePage { final DashboardViewModel walletViewModel; final WalletAddressListViewModel addressListViewModel; - final controller = PageController(initialPage: 0); + final controller = PageController(initialPage: 1); var pages = []; bool _isEffectsInstalled = false; @@ -221,7 +217,7 @@ class DashboardPage extends BasePage { if (_isEffectsInstalled) { return; } - + pages.add(MarketPlacePage(dashboardViewModel: walletViewModel)); pages.add(balancePage); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); _isEffectsInstalled = true; diff --git a/lib/src/screens/dashboard/widgets/market_place_page.dart b/lib/src/screens/dashboard/widgets/market_place_page.dart new file mode 100644 index 000000000..2484ae32a --- /dev/null +++ b/lib/src/screens/dashboard/widgets/market_place_page.dart @@ -0,0 +1,80 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/market_place_item.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class MarketPlacePage extends StatelessWidget { + + MarketPlacePage({@required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: RawScrollbar( + thumbColor: Colors.white.withOpacity(0.15), + radius: Radius.circular(20), + isAlwaysShown: true, + thickness: 2, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 50), + Text( + S.of(context).market_place, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ), + Expanded( + child: ListView( + controller: _scrollController, + children: [ + SizedBox(height: 20), + MarketPlaceItem( + onTap: () =>_navigatorToGiftCardsPage(context), + title: S.of(context).cake_pay_title, + subTitle: S.of(context).cake_pay_subtitle, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + void _navigatorToGiftCardsPage(BuildContext context) { + final walletType = dashboardViewModel.type; + + switch (walletType) { + case WalletType.haven: + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).gift_cards_unavailable, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + break; + default: + Navigator.of(context).pushNamed(Routes.ioniaWelcomePage); + } + } + +} diff --git a/lib/src/screens/ionia/auth/ionia_create_account_page.dart b/lib/src/screens/ionia/auth/ionia_create_account_page.dart new file mode 100644 index 000000000..5e2797768 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_create_account_page.dart @@ -0,0 +1,154 @@ +import 'package:cake_wallet/core/email_validator.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class IoniaCreateAccountPage extends BasePage { + IoniaCreateAccountPage(this._authViewModel) + : _emailFocus = FocusNode(), + _emailController = TextEditingController(), + _formKey = GlobalKey() { + _emailController.text = _authViewModel.email; + _emailController.addListener(() => _authViewModel.email = _emailController.text); + } + + final IoniaAuthViewModel _authViewModel; + + final GlobalKey _formKey; + + final FocusNode _emailFocus; + final TextEditingController _emailController; + + static const privacyPolicyUrl = 'https://ionia.docsend.com/view/jaqsmbq9w7dzvnqf'; + static const termsAndConditionsUrl = 'https://ionia.docsend.com/view/hi9awnwxr6mqgiqj'; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.sign_up, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _authViewModel.createUserState, (IoniaCreateAccountState state) { + if (state is IoniaCreateStateFailure) { + _onCreateUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onCreateSuccessful(context, _authViewModel); + } + }); + + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: BaseTextFormField( + hintText: S.of(context).email_address, + focusNode: _emailFocus, + validator: EmailValidator(), + keyboardType: TextInputType.emailAddress, + controller: _emailController, + ), + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).create_account, + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + await _authViewModel.createUser(_emailController.text); + }, + isLoading: _authViewModel.createUserState is IoniaCreateStateLoading, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox( + height: 20, + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: S.of(context).agree_to, + style: TextStyle( + color: Color(0xff7A93BA), + fontSize: 12, + fontFamily: 'Lato', + ), + children: [ + TextSpan( + text: S.of(context).settings_terms_and_conditions, + style: TextStyle( + color: Theme.of(context).accentTextTheme.body2.color, + fontWeight: FontWeight.w700, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + if (await canLaunch(termsAndConditionsUrl)) await launch(termsAndConditionsUrl); + }, + ), + TextSpan(text: ' ${S.of(context).and} '), + TextSpan( + text: S.of(context).privacy_policy, + style: TextStyle( + color: Theme.of(context).accentTextTheme.body2.color, + fontWeight: FontWeight.w700, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + if (await canLaunch(privacyPolicyUrl)) await launch(privacyPolicyUrl); + }), + TextSpan(text: ' ${S.of(context).by_cake_pay}'), + ], + ), + ), + ], + ), + ], + ), + ); + } + + void _onCreateUserFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.create_account, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onCreateSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed( + context, + Routes.ioniaVerifyIoniaOtpPage, + arguments: [authViewModel.email, false], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/ionia/auth/ionia_login_page.dart new file mode 100644 index 000000000..bcbc0fee3 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_login_page.dart @@ -0,0 +1,112 @@ +import 'package:cake_wallet/core/email_validator.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaLoginPage extends BasePage { + IoniaLoginPage(this._authViewModel) + : _formKey = GlobalKey(), + _emailController = TextEditingController() { + _emailController.text = _authViewModel.email; + _emailController.addListener(() => _authViewModel.email = _emailController.text); + } + + final GlobalKey _formKey; + + final IoniaAuthViewModel _authViewModel; + + @override + Color get titleColor => Colors.black; + + final TextEditingController _emailController; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.login, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _authViewModel.signInState, (IoniaCreateAccountState state) { + if (state is IoniaCreateStateFailure) { + _onLoginUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onLoginSuccessful(context, _authViewModel); + } + }); + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: BaseTextFormField( + hintText: S.of(context).email_address, + keyboardType: TextInputType.emailAddress, + validator: EmailValidator(), + controller: _emailController, + ), + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).login, + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + await _authViewModel.signIn(_emailController.text); + }, + isLoading: _authViewModel.signInState is IoniaCreateStateLoading, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ], + ), + ); + } + + void _onLoginUserFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.login, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onLoginSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed( + context, + Routes.ioniaVerifyIoniaOtpPage, + arguments: [authViewModel.email, true], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart new file mode 100644 index 000000000..c219781cc --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart @@ -0,0 +1,151 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaVerifyIoniaOtp extends BasePage { + IoniaVerifyIoniaOtp(this._authViewModel, this._email, this.isSignIn) + : _codeController = TextEditingController(), + _codeFocus = FocusNode() { + _codeController.addListener(() { + final otp = _codeController.text; + _authViewModel.otp = otp; + if (otp.length > 3) { + _authViewModel.otpState = IoniaOtpSendEnabled(); + } else { + _authViewModel.otpState = IoniaOtpSendDisabled(); + } + }); + } + + final IoniaAuthViewModel _authViewModel; + final bool isSignIn; + + final String _email; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.verification, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + final TextEditingController _codeController; + final FocusNode _codeFocus; + + @override + Widget body(BuildContext context) { + reaction((_) => _authViewModel.otpState, (IoniaOtpState state) { + if (state is IoniaOtpFailure) { + _onOtpFailure(context, state.error); + } + if (state is IoniaOtpSuccess) { + _onOtpSuccessful(context); + } + }); + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _codeFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + height: 0, + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).enter_code, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + focusNode: _codeFocus, + controller: _codeController, + ), + SizedBox(height: 14), + Text( + S.of(context).fill_code, + style: TextStyle(color: Color(0xff7A93BA), fontSize: 12), + ), + SizedBox(height: 34), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(S.of(context).dont_get_code), + SizedBox(width: 20), + InkWell( + onTap: () => isSignIn + ? _authViewModel.signIn(_email) + : _authViewModel.createUser(_email), + child: Text( + S.of(context).resend_code, + style: textSmallSemiBold(color: Palette.blueCraiola), + ), + ), + ], + ), + ], + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).continue_text, + onPressed: () async => await _authViewModel.verifyEmail(_codeController.text), + isDisabled: _authViewModel.otpState is IoniaOtpSendDisabled, + isLoading: _authViewModel.otpState is IoniaOtpValidating, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox(height: 20), + ], + ), + ], + ), + ), + ), + ); + } + + void _onOtpFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.verification, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onOtpSuccessful(BuildContext context) => + Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); +} diff --git a/lib/src/screens/ionia/auth/ionia_welcome_page.dart b/lib/src/screens/ionia/auth/ionia_welcome_page.dart new file mode 100644 index 000000000..bd01aed06 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_welcome_page.dart @@ -0,0 +1,104 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaWelcomePage extends BasePage { + IoniaWelcomePage(this._cardsListViewModel); + + @override + Widget middle(BuildContext context) { + return Text( + S.current.welcome_to_cakepay, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + final IoniaGiftCardsListViewModel _cardsListViewModel; + + @override + Widget body(BuildContext context) { + reaction((_) => _cardsListViewModel.isLoggedIn, (bool state) { + if (state) { + Navigator.pushReplacementNamed(context, Routes.ioniaManageCardsPage); + } + }); + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + SizedBox(height: 100), + Text( + S.of(context).about_cake_pay, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + SizedBox(height: 20), + Text( + S.of(context).cake_pay_account_note, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PrimaryButton( + text: S.of(context).create_account, + onPressed: () => Navigator.of(context).pushNamed(Routes.ioniaCreateAccountPage), + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + SizedBox( + height: 16, + ), + Text( + S.of(context).already_have_account, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + SizedBox(height: 8), + InkWell( + onTap: () => Navigator.of(context).pushNamed(Routes.ioniaLoginPage), + child: Text( + S.of(context).login, + style: TextStyle( + color: Palette.blueCraiola, + fontSize: 18, + letterSpacing: 1.5, + fontWeight: FontWeight.w900, + ), + ), + ), + SizedBox(height: 20) + ], + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart new file mode 100644 index 000000000..f1d08a75e --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart @@ -0,0 +1,181 @@ +import 'dart:ffi'; + +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class IoniaAccountCardsPage extends BasePage { + IoniaAccountCardsPage(this.ioniaAccountViewModel); + + final IoniaAccountViewModel ioniaAccountViewModel; + + @override + Widget middle(BuildContext context) { + return Text( + S.of(context).cards, + style: textLargeSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + return _IoniaCardTabs(ioniaAccountViewModel); + } +} + +class _IoniaCardTabs extends StatefulWidget { + _IoniaCardTabs(this.ioniaAccountViewModel); + + final IoniaAccountViewModel ioniaAccountViewModel; + + @override + _IoniaCardTabsState createState() => _IoniaCardTabsState(); +} + +class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProviderStateMixin { + TabController _tabController; + + @override + void initState() { + _tabController = TabController(length: 2, vsync: this); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _tabController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 45, + width: 230, + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), + borderRadius: BorderRadius.circular( + 25.0, + ), + ), + child: Theme( + data: ThemeData(primaryTextTheme: TextTheme(body2: TextStyle(backgroundColor: Colors.transparent))), + child: TabBar( + controller: _tabController, + indicator: BoxDecoration( + borderRadius: BorderRadius.circular( + 25.0, + ), + color: Theme.of(context).accentTextTheme.body2.color, + ), + labelColor: Theme.of(context).primaryTextTheme.display4.backgroundColor, + unselectedLabelColor: Theme.of(context).primaryTextTheme.title.color, + tabs: [ + Tab( + text: S.of(context).active, + ), + Tab( + text: S.of(context).redeemed, + ), + ], + ), + ), + ), + SizedBox(height: 16), + Expanded( + child: Observer(builder: (_) { + final viewModel = widget.ioniaAccountViewModel; + return TabBarView( + controller: _tabController, + children: [ + _IoniaCardListView( + emptyText: S.of(context).gift_card_balance_note, + merchList: viewModel.activeMechs, + onTap: (giftCard) { + Navigator.pushNamed( + context, + Routes.ioniaGiftCardDetailPage, + arguments: [giftCard]) + .then((_) => viewModel.updateUserGiftCards()); + }), + _IoniaCardListView( + emptyText: S.of(context).gift_card_redeemed_note, + merchList: viewModel.redeemedMerchs, + onTap: (giftCard) { + Navigator.pushNamed( + context, + Routes.ioniaGiftCardDetailPage, + arguments: [giftCard]) + .then((_) => viewModel.updateUserGiftCards()); + }), + ], + ); + }), + ), + ], + ), + ); + } +} + +class _IoniaCardListView extends StatelessWidget { + _IoniaCardListView({ + Key key, + @required this.emptyText, + @required this.merchList, + @required this.onTap, + }) : super(key: key); + + final String emptyText; + final List merchList; + final void Function(IoniaGiftCard giftCard) onTap; + + @override + Widget build(BuildContext context) { + return merchList.isEmpty + ? Center( + child: Text( + emptyText, + textAlign: TextAlign.center, + style: textSmall( + color: Theme.of(context).primaryTextTheme.overline.color, + ), + ), + ) + : ListView.builder( + itemCount: merchList.length, + itemBuilder: (context, index) { + final merchant = merchList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: CardItem( + onTap: () => onTap?.call(merchant), + title: merchant.legalName, + backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), + discount: 0, + discountBackground: AssetImage('assets/images/red_badge_discount.png'), + titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, + subtitleColor: Theme.of(context).hintColor, + subTitle: '', + logoUrl: merchant.logoUrl, + ), + ); + }, + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart new file mode 100644 index 000000000..28e7e9b42 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_account_page.dart @@ -0,0 +1,181 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class IoniaAccountPage extends BasePage { + IoniaAccountPage(this.ioniaAccountViewModel); + + final IoniaAccountViewModel ioniaAccountViewModel; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.account, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + _GradiantContainer( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Observer( + builder: (_) => RichText( + text: TextSpan( + text: '${ioniaAccountViewModel.countOfMerch}', + style: textLargeSemiBold(), + children: [ + TextSpan( + text: ' ${S.of(context).active_cards}', + style: textSmall(color: Colors.white.withOpacity(0.7))), + ], + ), + )), + InkWell( + onTap: () { + Navigator.pushNamed(context, Routes.ioniaAccountCardsPage) + .then((_) => ioniaAccountViewModel.updateUserGiftCards()); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + S.of(context).view_all, + style: textSmallSemiBold(), + ), + ), + ) + ], + ), + ), + SizedBox(height: 8), + //Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _GradiantContainer( + // padding: EdgeInsets.all(16), + // width: deviceWidth * 0.28, + // content: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // S.of(context).total_saving, + // style: textSmall(), + // ), + // SizedBox(height: 8), + // Text( + // '\$100', + // style: textMediumSemiBold(), + // ), + // ], + // ), + // ), + // _GradiantContainer( + // padding: EdgeInsets.all(16), + // width: deviceWidth * 0.28, + // content: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // S.of(context).last_30_days, + // style: textSmall(), + // ), + // SizedBox(height: 8), + // Text( + // '\$100', + // style: textMediumSemiBold(), + // ), + // ], + // ), + // ), + // _GradiantContainer( + // padding: EdgeInsets.all(16), + // width: deviceWidth * 0.28, + // content: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // S.of(context).avg_savings, + // style: textSmall(), + // ), + // SizedBox(height: 8), + // Text( + // '10%', + // style: textMediumSemiBold(), + // ), + // ], + // ), + // ), + // ], + //), + SizedBox(height: 40), + Observer( + builder: (_) => IoniaTile(title: S.of(context).email_address, subTitle: ioniaAccountViewModel.email), + ), + Divider() + ], + ), + bottomSectionPadding: EdgeInsets.all(30), + bottomSection: Column( + children: [ + PrimaryButton( + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + text: S.of(context).logout, + onPressed: () { + ioniaAccountViewModel.logout(); + Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); + }, + ), + ], + ), + ); + } +} + +class _GradiantContainer extends StatelessWidget { + const _GradiantContainer({ + Key key, + @required this.content, + this.padding, + this.width, + }) : super(key: key); + + final Widget content; + final EdgeInsets padding; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + child: content, + width: width, + padding: padding ?? EdgeInsets.all(24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).accentColor, + ], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart new file mode 100644 index 000000000..4f688a129 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart @@ -0,0 +1,114 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaActivateDebitCardPage extends BasePage { + + IoniaActivateDebitCardPage(this._cardsListViewModel); + + final IoniaGiftCardsListViewModel _cardsListViewModel; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.debit_card, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _cardsListViewModel.createCardState, (IoniaCreateCardState state) { + if (state is IoniaCreateCardFailure) { + _onCreateCardFailure(context, state.error); + } + if (state is IoniaCreateCardSuccess) { + _onCreateCardSuccess(context); + } + }); + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SizedBox(height: 16), + Text(S.of(context).debit_card_terms), + SizedBox(height: 24), + Text(S.of(context).please_reference_document), + SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + TextIconButton( + label: S.current.cardholder_agreement, + onTap: () {}, + ), + SizedBox( + height: 24, + ), + TextIconButton( + label: S.current.e_sign_consent, + onTap: () {}, + ), + ], + ), + ), + ], + ), + ), + bottomSection: LoadingPrimaryButton( + onPressed: () { + _cardsListViewModel.createCard(); + }, + isLoading: _cardsListViewModel.createCardState is IoniaCreateCardLoading, + text: S.of(context).agree_and_continue, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ); + } + + void _onCreateCardFailure(BuildContext context, String errorMessage) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: errorMessage, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onCreateCardSuccess(BuildContext context) { + Navigator.pushNamed( + context, + Routes.ioniaDebitCardPage, + ); + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).congratulations, + alertContent: S.of(context).you_now_have_debit_card, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart new file mode 100644 index 000000000..95202f472 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -0,0 +1,492 @@ +import 'dart:ui'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/confirm_modal.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/discount_badge.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; + +class IoniaBuyGiftCardDetailPage extends BasePage { + IoniaBuyGiftCardDetailPage(this.ioniaPurchaseViewModel); + + final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel; + + @override + Widget middle(BuildContext context) { + return Text( + ioniaPurchaseViewModel.ioniaMerchant.legalName, + style: textMediumSemiBold(color: Theme.of(context).accentTextTheme.display4.backgroundColor), + ); + } + + @override + Widget trailing(BuildContext context) + => ioniaPurchaseViewModel.ioniaMerchant.discount > 0 + ? DiscountBadge(percentage: ioniaPurchaseViewModel.ioniaMerchant.discount) + : null; + + @override + Widget body(BuildContext context) { + reaction((_) => ioniaPurchaseViewModel.invoiceCreationState, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } + }); + + reaction((_) => ioniaPurchaseViewModel.invoiceCommittingState, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } + + if (state is ExecutedSuccessfullyState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pushReplacementNamed( + Routes.ioniaPaymentStatusPage, + arguments: [ + ioniaPurchaseViewModel.paymentInfo, + ioniaPurchaseViewModel.committedInfo]); + }); + } + }); + + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Observer(builder: (_) { + final tipAmount = ioniaPurchaseViewModel.tipAmount; + return Column( + children: [ + SizedBox(height: 36), + Container( + padding: EdgeInsets.symmetric(vertical: 24), + margin: EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + children: [ + Text( + S.of(context).gift_card_amount, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$${ioniaPurchaseViewModel.giftCardAmount.toStringAsFixed(2)}', + style: textXLargeSemiBold(), + ), + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).bill_amount, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$${ioniaPurchaseViewModel.billAmount.toStringAsFixed(2)}', + style: textLargeSemiBold(), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + S.of(context).tip, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$${tipAmount.toStringAsFixed(2)}', + style: textLargeSemiBold(), + ), + ], + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).tip, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.title.color, + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + SizedBox(height: 4), + Observer( + builder: (_) => TipButtonGroup( + selectedTip: ioniaPurchaseViewModel.selectedTip.percentage, + tipsList: ioniaPurchaseViewModel.tips, + onSelect: (value) => ioniaPurchaseViewModel.addTip(value), + ), + ) + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: TextIconButton( + label: S.of(context).how_to_use_card, + onTap: () => _showHowToUseCard(context, ioniaPurchaseViewModel.ioniaMerchant), + ), + ), + ], + ); + }), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: Observer(builder: (_) { + return LoadingPrimaryButton( + isLoading: ioniaPurchaseViewModel.invoiceCreationState is IsExecutingState || + ioniaPurchaseViewModel.invoiceCommittingState is IsExecutingState, + onPressed: () => purchaseCard(context), + text: S.of(context).purchase_gift_card, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ); + }), + ), + SizedBox(height: 8), + InkWell( + onTap: () => _showTermsAndCondition(context), + child: Text(S.of(context).settings_terms_and_conditions, + style: textMediumSemiBold( + color: Theme.of(context).primaryTextTheme.body1.color, + ).copyWith(fontSize: 12)), + ), + SizedBox(height: 16) + ], + ), + ); + } + + void _showTermsAndCondition(BuildContext context) { + showPopUp( + context: context, + builder: (BuildContext context) { + return IoniaAlertModal( + title: S.of(context).settings_terms_and_conditions, + content: Align( + alignment: Alignment.bottomLeft, + child: Text( + ioniaPurchaseViewModel.ioniaMerchant.termsAndConditions, + style: textMedium( + color: Theme.of(context).textTheme.display2.color, + ), + ), + ), + actionTitle: S.of(context).agree, + showCloseButton: false, + heightFactor: 0.6, + ); + }); + } + + Future purchaseCard(BuildContext context) async { + await ioniaPurchaseViewModel.createInvoice(); + + if (ioniaPurchaseViewModel.invoiceCreationState is ExecutedSuccessfullyState) { + await _presentSuccessfulInvoiceCreationPopup(context); + } + } + + void _showHowToUseCard( + BuildContext context, + IoniaMerchant merchant, + ) { + showPopUp( + context: context, + builder: (BuildContext context) { + return IoniaAlertModal( + title: S.of(context).how_to_use_card, + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: merchant.instructions + .map((instruction) { + return [ + Padding( + padding: EdgeInsets.all(10), + child: Text( + instruction.header, + style: textLargeSemiBold( + color: Theme.of(context).textTheme.display2.color, + ), + )), + Text( + instruction.body, + style: textMedium( + color: Theme.of(context).textTheme.display2.color, + ), + ) + ]; + }) + .expand((e) => e) + .toList()), + actionTitle: S.current.send_got_it, + ); + }); + } + + Future _presentSuccessfulInvoiceCreationPopup(BuildContext context) async { + final amount = ioniaPurchaseViewModel.invoice.totalAmount; + final addresses = ioniaPurchaseViewModel.invoice.outAddresses; + + await showPopUp( + context: context, + builder: (_) { + return IoniaConfirmModal( + alertTitle: S.of(context).confirm_sending, + alertContent: Container( + height: 200, + padding: EdgeInsets.all(15), + child: Column(children: [ + Row(children: [ + Text(S.of(context).payment_id, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)), + Text(ioniaPurchaseViewModel.invoice.paymentId, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)) + ], mainAxisAlignment: MainAxisAlignment.spaceBetween), + SizedBox(height: 10), + Row(children: [ + Text(S.of(context).amount, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)), + Text('$amount ${ioniaPurchaseViewModel.invoice.chain}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)) + ], mainAxisAlignment: MainAxisAlignment.spaceBetween), + SizedBox(height: 25), + Row(children: [ + Text(S.of(context).recipient_address, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)) + ], mainAxisAlignment: MainAxisAlignment.center), + Expanded( + child: ListView.builder( + itemBuilder: (_, int index) { + return Text(addresses[index], + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none)); + }, + itemCount: addresses.length, + physics: NeverScrollableScrollPhysics())) + ])), + rightButtonText: S.of(context).ok, + leftButtonText: S.of(context).cancel, + leftActionColor: Color(0xffFF6600), + rightActionColor: Theme.of(context).accentTextTheme.body2.color, + actionRightButton: () async { + Navigator.of(context).pop(); + await ioniaPurchaseViewModel.commitPaymentInvoice(); + }, + actionLeftButton: () => Navigator.of(context).pop()); + }, + ); + } +} + +class TipButtonGroup extends StatelessWidget { + const TipButtonGroup({ + Key key, + @required this.selectedTip, + @required this.onSelect, + @required this.tipsList, + }) : super(key: key); + + final Function(IoniaTip) onSelect; + final double selectedTip; + final List tipsList; + + bool _isSelected(double value) => selectedTip == value; + + @override + Widget build(BuildContext context) { + return Container( + height: 50, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: tipsList.length, + itemBuilder: (BuildContext context, int index) { + final tip = tipsList[index]; + return Padding( + padding: EdgeInsets.only(right: 5), + child: TipButton( + isSelected: _isSelected(tip.percentage), + onTap: () => onSelect(tip), + caption: '${tip.percentage}%', + subTitle: '\$${tip.additionalAmount}', + )); + })); + } +} + +class TipButton extends StatelessWidget { + const TipButton({ + @required this.caption, + this.subTitle, + @required this.onTap, + this.isSelected = false, + }); + + final String caption; + final String subTitle; + final bool isSelected; + final void Function() onTap; + + bool isDark(BuildContext context) => Theme.of(context).brightness == Brightness.dark; + + Color captionTextColor(BuildContext context) { + if (isDark(context)) { + return Theme.of(context).primaryTextTheme.title.color; + } + + return isSelected + ? Theme.of(context).accentTextTheme.title.color + : Theme.of(context).primaryTextTheme.title.color; + } + + Color subTitleTextColor(BuildContext context) { + if (isDark(context)) { + return Theme.of(context).primaryTextTheme.title.color; + } + + return isSelected + ? Theme.of(context).accentTextTheme.title.color + : Theme.of(context).primaryTextTheme.overline.color; + } + + Color backgroundColor(BuildContext context) { + if (isDark(context)) { + return isSelected + ? null + : Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.01); + } + + return isSelected + ? null + : Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1); + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: 49, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(caption, + style: textSmallSemiBold( + color: captionTextColor(context))), + if (subTitle != null) ...[ + SizedBox(height: 4), + Text( + subTitle, + style: textXxSmallSemiBold( + color: subTitleTextColor(context), + ), + ), + ] + ], + ), + padding: EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: backgroundColor(context), + gradient: isSelected + ? LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart new file mode 100644 index 000000000..899b1387d --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -0,0 +1,185 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaBuyGiftCardPage extends BasePage { + IoniaBuyGiftCardPage( + this.ioniaBuyCardViewModel, + ) : _amountFieldFocus = FocusNode(), + _amountController = TextEditingController() { + _amountController.addListener(() { + ioniaBuyCardViewModel.onAmountChanged(_amountController.text); + }); + } + + final IoniaBuyCardViewModel ioniaBuyCardViewModel; + + @override + String get title => S.current.enter_amount; + + @override + Color get titleColor => Colors.white; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); + + final TextEditingController _amountController; + final FocusNode _amountFieldFocus; + + @override + Widget body(BuildContext context) { + final _width = MediaQuery.of(context).size.width; + final merchant = ioniaBuyCardViewModel.ioniaMerchant; + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFieldFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 150), + BaseTextFormField( + controller: _amountController, + focusNode: _amountFieldFocus, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), + WhitelistingTextInputFormatter(RegExp(r'^\d+(\.|\,)?\d{0,2}'))], + hintText: '1000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + fontWeight: FontWeight.w600, + fontSize: 36, + ), + borderColor: Theme.of(context).primaryTextTheme.headline.color, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + fontSize: 36, + ), + suffixIcon: SizedBox( + width: _width / 6, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + top: 5.0, + left: _width / 4, + ), + child: Text( + 'USD: ', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 36, + ), + ), + ), + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).min_amount(merchant.minimumCardPurchase.toStringAsFixed(2)), + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ), + ), + Text( + S.of(context).max_amount(merchant.maximumCardPurchase.toStringAsFixed(2)), + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ), + ), + ], + ), + SizedBox(height: 24), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CardItem( + title: merchant.legalName, + backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), + discount: merchant.discount, + titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, + subtitleColor: Theme.of(context).hintColor, + subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline, + logoUrl: merchant.logoUrl, + ), + ) + ], + ), + bottomSection: Column( + children: [ + Observer(builder: (_) { + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed( + Routes.ioniaBuyGiftCardDetailPage, + arguments: [ + ioniaBuyCardViewModel.amount, + ioniaBuyCardViewModel.ioniaMerchant, + ], + ), + text: S.of(context).continue_text, + isDisabled: !ioniaBuyCardViewModel.isEnablePurchase, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ); + }), + SizedBox(height: 30), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart new file mode 100644 index 000000000..5dbce02b6 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart @@ -0,0 +1,176 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaCustomTipPage extends BasePage { + IoniaCustomTipPage( + this.ioniaPurchaseViewModel, + ) : _amountFieldFocus = FocusNode(), + _amountController = TextEditingController() { + _amountController.addListener(() { + // ioniaPurchaseViewModel.onTipChanged(_amountController.text); + }); + } + + final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel; + + + @override + String get title => S.current.enter_amount; + + @override + Color get titleColor => Colors.white; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); + + final TextEditingController _amountController; + final FocusNode _amountFieldFocus; + + @override + Widget body(BuildContext context) { + final _width = MediaQuery.of(context).size.width; + final merchant = ioniaPurchaseViewModel.ioniaMerchant; + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFieldFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 150), + BaseTextFormField( + controller: _amountController, + focusNode: _amountFieldFocus, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))], + hintText: '1000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + fontWeight: FontWeight.w500, + fontSize: 36, + ), + borderColor: Theme.of(context).primaryTextTheme.headline.color, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + fontSize: 36, + ), + suffixIcon: SizedBox( + width: _width / 6, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + top: 5.0, + left: _width / 4, + ), + child: Text( + 'USD: ', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 36, + ), + ), + ), + ), + SizedBox(height: 8), + Observer(builder: (_) { + if (ioniaPurchaseViewModel.percentage == 0.0) { + return SizedBox.shrink(); + } + + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: '\$${_amountController.text}', + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ), + children: [ + TextSpan(text: ' ${S.of(context).is_percentage} '), + TextSpan(text: '${ioniaPurchaseViewModel.percentage}%'), + TextSpan(text: ' ${S.of(context).percentageOf(ioniaPurchaseViewModel.amount.toString())} '), + ], + ), + ); + }), + SizedBox(height: 24), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CardItem( + title: merchant.legalName, + backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), + discount: 0.0, + titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, + subtitleColor: Theme.of(context).hintColor, + subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline, + logoUrl: merchant.logoUrl, + ), + ) + ], + ), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () { + Navigator.of(context).pop(_amountController.text); + }, + text: S.of(context).add_tip, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart new file mode 100644 index 000000000..f863f5780 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart @@ -0,0 +1,379 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class IoniaDebitCardPage extends BasePage { + final IoniaGiftCardsListViewModel _cardsListViewModel; + + IoniaDebitCardPage(this._cardsListViewModel); + + @override + Widget middle(BuildContext context) { + return Text( + S.current.debit_card, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + return Observer( + builder: (_) { + final cardState = _cardsListViewModel.cardState; + if (cardState is IoniaFetchingCard) { + return Center(child: CircularProgressIndicator()); + } + if (cardState is IoniaCardSuccess) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: _IoniaDebitCard( + cardInfo: cardState.card, + ), + ), + bottomSection: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + S.of(context).billing_address_info, + style: textSmall(color: Theme.of(context).textTheme.display1.color), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 24), + PrimaryButton( + text: S.of(context).order_physical_card, + onPressed: () {}, + color: Color(0xffE9F2FC), + textColor: Theme.of(context).textTheme.display2.color, + ), + SizedBox(height: 8), + PrimaryButton( + text: S.of(context).add_value, + onPressed: () {}, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + SizedBox(height: 16) + ], + ), + ); + } + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _IoniaDebitCard(isCardSample: true), + SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + TextIconButton( + label: S.current.how_to_use_card, + onTap: () => _showHowToUseCard(context), + ), + SizedBox( + height: 24, + ), + TextIconButton( + label: S.current.frequently_asked_questions, + onTap: () {}, + ), + ], + ), + ), + SizedBox(height: 50), + Container( + padding: EdgeInsets.all(20), + margin: EdgeInsets.all(8), + width: double.infinity, + decoration: BoxDecoration( + color: Color.fromRGBO(233, 242, 252, 1), + borderRadius: BorderRadius.circular(20), + ), + child: RichText( + text: TextSpan( + text: S.of(context).get_a, + style: textMedium(color: Theme.of(context).textTheme.display2.color), + children: [ + TextSpan( + text: S.of(context).digital_and_physical_card, + style: textMediumBold(color: Theme.of(context).textTheme.display2.color), + ), + TextSpan( + text: S.of(context).get_card_note, + ) + ], + )), + ), + ], + ), + ), + bottomSectionPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 32, + ), + bottomSection: PrimaryButton( + text: S.of(context).activate, + onPressed: () => _showHowToUseCard(context, activate: true), + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ); + }, + ); + } + + void _showHowToUseCard(BuildContext context, {bool activate = false}) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertBackground( + child: Material( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 10), + Container( + padding: EdgeInsets.only(top: 24, left: 24, right: 24), + margin: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + children: [ + Text( + S.of(context).how_to_use_card, + style: textLargeSemiBold( + color: Theme.of(context).textTheme.body1.color, + ), + ), + SizedBox(height: 24), + Align( + alignment: Alignment.bottomLeft, + child: Text( + S.of(context).signup_for_card_accept_terms, + style: textSmallSemiBold( + color: Theme.of(context).textTheme.display2.color, + ), + ), + ), + SizedBox(height: 24), + _TitleSubtitleTile( + title: S.of(context).add_fund_to_card('1000'), + subtitle: S.of(context).use_card_info_two, + ), + SizedBox(height: 21), + _TitleSubtitleTile( + title: S.of(context).use_card_info_three, + subtitle: S.of(context).optionally_order_card, + ), + SizedBox(height: 35), + PrimaryButton( + onPressed: () => activate + ? Navigator.pushNamed(context, Routes.ioniaActivateDebitCardPage) + : Navigator.pop(context), + text: S.of(context).send_got_it, + color: Color.fromRGBO(233, 242, 252, 1), + textColor: Theme.of(context).textTheme.display2.color, + ), + SizedBox(height: 21), + ], + ), + ), + InkWell( + onTap: () => Navigator.pop(context), + child: Container( + margin: EdgeInsets.only(bottom: 40), + child: CircleAvatar( + child: Icon( + Icons.close, + color: Colors.black, + ), + backgroundColor: Colors.white, + ), + ), + ) + ], + ), + ), + ); + }); + } +} + +class _IoniaDebitCard extends StatefulWidget { + final bool isCardSample; + final IoniaVirtualCard cardInfo; + const _IoniaDebitCard({ + Key key, + this.isCardSample = false, + this.cardInfo, + }) : super(key: key); + + @override + _IoniaDebitCardState createState() => _IoniaDebitCardState(); +} + +class _IoniaDebitCardState extends State<_IoniaDebitCard> { + bool _showDetails = false; + void _toggleVisibility() { + setState(() => _showDetails = !_showDetails); + } + + String _formatPan(String pan) { + if (pan == null) return ''; + return pan.replaceAllMapped(RegExp(r'.{4}'), (match) => '${match.group(0)} '); + } + + String get _getLast4 => widget.isCardSample ? '0000' : widget.cardInfo.pan.substring(widget.cardInfo.pan.length - 5); + + String get _getSpendLimit => widget.isCardSample ? '10000' : widget.cardInfo.spendLimit.toStringAsFixed(2); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 19), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.current.cakepay_prepaid_card, + style: textSmall(), + ), + Image.asset( + 'assets/images/mastercard.png', + width: 54, + ), + ], + ), + Text( + widget.isCardSample ? S.of(context).upto(_getSpendLimit) : '\$$_getSpendLimit', + style: textXLargeSemiBold(), + ), + SizedBox(height: 16), + Text( + _showDetails ? _formatPan(widget.cardInfo.pan) : '**** **** **** $_getLast4', + style: textMediumSemiBold(), + ), + SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.isCardSample) + Text( + S.current.no_id_needed, + style: textMediumBold(), + ) + else ...[ + Column( + children: [ + Text( + 'CVV', + style: textXSmallSemiBold(), + ), + SizedBox(height: 4), + Text( + _showDetails ? widget.cardInfo.cvv : '***', + style: textMediumSemiBold(), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).expires, + style: textXSmallSemiBold(), + ), + SizedBox(height: 4), + Text( + '${widget.cardInfo.expirationMonth ?? S.of(context).mm}/${widget.cardInfo.expirationYear ?? S.of(context).yy}', + style: textMediumSemiBold(), + ) + ], + ), + ] + ], + ), + if (!widget.isCardSample) ...[ + SizedBox(height: 8), + Center( + child: InkWell( + onTap: () => _toggleVisibility(), + child: Text( + _showDetails ? S.of(context).hide_details : S.of(context).show_details, + style: textSmall(), + ), + ), + ), + ], + ], + ), + ); + } +} + +class _TitleSubtitleTile extends StatelessWidget { + final String title; + final String subtitle; + const _TitleSubtitleTile({ + Key key, + @required this.title, + @required this.subtitle, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textSmallSemiBold(color: Theme.of(context).textTheme.display2.color), + ), + SizedBox(height: 4), + Text( + subtitle, + style: textSmall(color: Theme.of(context).textTheme.display2.color), + ), + ], + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart new file mode 100644 index 000000000..9aa094f4e --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -0,0 +1,188 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaGiftCardDetailPage extends BasePage { + IoniaGiftCardDetailPage(this.viewModel); + + final IoniaGiftCardDetailsViewModel viewModel; + + @override + Widget leading(BuildContext context) { + if (ModalRoute.of(context).isFirst) { + return null; + } + + final _backButton = Icon( + Icons.arrow_back_ios, + color: Theme.of(context).primaryTextTheme.title.color, + size: 16, + ); + return Padding( + padding: const EdgeInsets.only(left: 10.0), + child: SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => onClose(context), + child: _backButton), + ), + ), + ); + } + + @override + Widget middle(BuildContext context) { + return Text( + viewModel.giftCard.legalName, + style: textMediumSemiBold(color: Theme.of(context).accentTextTheme.display4.backgroundColor), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => viewModel.redeemState, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } + }); + + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + if (viewModel.giftCard.barcodeUrl != null && viewModel.giftCard.barcodeUrl.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 24, + ), + child: Image.network(viewModel.giftCard.barcodeUrl), + ), + SizedBox(height: 24), + buildIoniaTile( + context, + title: S.of(context).gift_card_number, + subTitle: viewModel.giftCard.cardNumber, + ), + if (viewModel.giftCard.cardPin?.isNotEmpty ?? false) + ...[Divider(height: 30), + buildIoniaTile( + context, + title: S.of(context).pin_number, + subTitle: viewModel.giftCard.cardPin, + )], + Divider(height: 30), + Observer(builder: (_) => + buildIoniaTile( + context, + title: S.of(context).amount, + subTitle: viewModel.giftCard.remainingAmount.toStringAsFixed(2) ?? '0.00', + )), + Divider(height: 50), + TextIconButton( + label: S.of(context).how_to_use_card, + onTap: () => _showHowToUseCard(context, viewModel.giftCard), + ), + ], + ), + bottomSection: Padding( + padding: EdgeInsets.only(bottom: 12), + child: Observer(builder: (_) { + if (!viewModel.giftCard.isEmpty) { + return LoadingPrimaryButton( + isLoading: viewModel.redeemState is IsExecutingState, + onPressed: () => viewModel.redeem().then((_){ + Navigator.of(context).pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); + }), + text: S.of(context).mark_as_redeemed, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white); + } + + return Container(); + })), + ); + } + + Widget buildIoniaTile(BuildContext context, {@required String title, @required String subTitle}) { + return IoniaTile( + title: title, + subTitle: subTitle, + onTap: () { + Clipboard.setData(ClipboardData(text: subTitle)); + showBar(context, + S.of(context).transaction_details_copied(title)); + }); + } + + void _showHowToUseCard( + BuildContext context, + IoniaGiftCard merchant, + ) { + showPopUp( + context: context, + builder: (BuildContext context) { + return IoniaAlertModal( + title: S.of(context).how_to_use_card, + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: viewModel.giftCard.instructions + .map((instruction) { + return [ + Padding( + padding: EdgeInsets.all(10), + child: Text( + instruction.header, + style: textLargeSemiBold( + color: Theme.of(context).textTheme.display2.color, + ), + )), + Text( + instruction.body, + style: textMedium( + color: Theme.of(context).textTheme.display2.color, + ), + ) + ]; + }) + .expand((e) => e) + .toList()), + actionTitle: S.of(context).send_got_it, + ); + }); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart new file mode 100644 index 000000000..656b8676b --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -0,0 +1,340 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_menu.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/ionia_filter_modal.dart'; +import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class IoniaManageCardsPage extends BasePage { + IoniaManageCardsPage(this._cardsListViewModel) { + _searchController.addListener(() { + if (_searchController.text != _cardsListViewModel.searchString) { + _searchDebounce.run(() { + _cardsListViewModel.searchMerchant(_searchController.text); + }); + } + }); + } + final IoniaGiftCardsListViewModel _cardsListViewModel; + + final _searchDebounce = Debounce(Duration(milliseconds: 500)); + final _searchController = TextEditingController(); + + @override + Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; + + @override + Color get backgroundDarkColor => Colors.transparent; + + @override + Color get titleColor => currentTheme.type == ThemeType.bright ? Colors.white : Colors.black; + + @override + Widget Function(BuildContext, Widget) get rootWrapper => (BuildContext context, Widget scaffold) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).accentColor, + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor, + ], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + child: scaffold, + ); + + @override + bool get resizeToAvoidBottomInset => false; + + @override + Widget get endDrawer => CardMenu(); + + @override + Widget leading(BuildContext context) { + final _backButton = Icon( + Icons.arrow_back_ios, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + size: 16, + ); + + return SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => Navigator.pop(context), + child: _backButton), + ), + ); + } + + @override + Widget middle(BuildContext context) { + return Text( + S.of(context).gift_cards, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ); + } + + @override + Widget trailing(BuildContext context) { + return _TrailingIcon( + asset: 'assets/images/profile.png', + onPressed: () => Navigator.pushNamed(context, Routes.ioniaAccountPage), + ); + } + + @override + Widget body(BuildContext context) { + final filterIcon = InkWell( + onTap: () async { + final selectedFilters = await showCategoryFilter(context, _cardsListViewModel); + _cardsListViewModel.setSelectedFilter(selectedFilters); + }, + child: Image.asset( + 'assets/images/filter.png', + color: Theme.of(context).textTheme.caption.decorationColor, + )); + + return Padding( + padding: const EdgeInsets.all(14.0), + child: Column( + children: [ + Container( + padding: EdgeInsets.only(left: 2, right: 22), + height: 32, + child: Row( + children: [ + Expanded( + child: _SearchWidget( + controller: _searchController, + )), + SizedBox(width: 10), + Container( + width: 32, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + child: filterIcon, + ) + ], + ), + ), + SizedBox(height: 8), + Expanded( + child: IoniaManageCardsPageBody( + cardsListViewModel: _cardsListViewModel, + ), + ), + ], + ), + ); + } + + Future> showCategoryFilter( + BuildContext context, + IoniaGiftCardsListViewModel viewModel, + ) async { + return await showPopUp>( + context: context, + builder: (BuildContext context) { + return IoniaFilterModal( + filterViewModel: getIt.get(), + selectedCategories: viewModel.selectedFilters, + ); + }, + ); + } +} + +class IoniaManageCardsPageBody extends StatefulWidget { + const IoniaManageCardsPageBody({ + Key key, + @required this.cardsListViewModel, + }) : super(key: key); + + final IoniaGiftCardsListViewModel cardsListViewModel; + + @override + _IoniaManageCardsPageBodyState createState() => _IoniaManageCardsPageBodyState(); +} + +class _IoniaManageCardsPageBodyState extends State { + double get backgroundHeight => MediaQuery.of(context).size.height * 0.75; + double thumbHeight = 72; + bool get isAlwaysShowScrollThumb => merchantsList == null ? false : merchantsList.length > 3; + + List get merchantsList => widget.cardsListViewModel.ioniaMerchants; + + final _scrollController = ScrollController(); + + @override + void initState() { + _scrollController.addListener(() { + final scrollOffsetFromTop = _scrollController.hasClients + ? (_scrollController.offset / _scrollController.position.maxScrollExtent * (backgroundHeight - thumbHeight)) + : 0.0; + widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) => Stack(children: [ + ListView.separated( + padding: EdgeInsets.only(left: 2, right: 22), + controller: _scrollController, + itemCount: merchantsList.length, + separatorBuilder: (_, __) => SizedBox(height: 4), + itemBuilder: (_, index) { + final merchant = merchantsList[index]; + var subTitle = ''; + + if (merchant.isOnline) { + subTitle += S.of(context).online; + } + + if (merchant.isPhysical) { + if (subTitle.isNotEmpty) { + subTitle = '$subTitle & '; + } + + subTitle = '${subTitle}${S.of(context).in_store}'; + } + + return CardItem( + logoUrl: merchant.logoUrl, + onTap: () { + Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardPage, arguments: [merchant]); + }, + title: merchant.legalName, + subTitle: subTitle, + backgroundColor: Theme.of(context).textTheme.title.backgroundColor, + titleColor: Theme.of(context).accentTextTheme.display3.backgroundColor, + subtitleColor: Theme.of(context).accentTextTheme.display2.backgroundColor, + discount: merchant.discount, + ); + }, + ), + isAlwaysShowScrollThumb + ? CakeScrollbar( + backgroundHeight: backgroundHeight, + thumbHeight: thumbHeight, + rightOffset: 1, + width: 3, + backgroundColor: Theme.of(context).textTheme.caption.decorationColor.withOpacity(0.05), + thumbColor: Theme.of(context).textTheme.caption.decorationColor.withOpacity(0.5), + fromTop: widget.cardsListViewModel.scrollOffsetFromTop, + ) + : Offstage() + ]), + ); + } +} + +class _SearchWidget extends StatelessWidget { + const _SearchWidget({ + Key key, + @required this.controller, + }) : super(key: key); + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + final searchIcon = Padding( + padding: EdgeInsets.all(8), + child: Image.asset( + 'assets/images/mini_search_icon.png', + color: Theme.of(context).textTheme.caption.decorationColor, + ), + ); + + return TextField( + style: TextStyle(color: Colors.white), + controller: controller, + decoration: InputDecoration( + filled: true, + contentPadding: EdgeInsets.only( + top: 10, + left: 10, + ), + fillColor: Colors.white.withOpacity(0.15), + hintText: S.of(context).search, + hintStyle: TextStyle( + color: Colors.white.withOpacity(0.6), + ), + alignLabelWithHint: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + suffixIcon: searchIcon, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withOpacity(0.2)), + borderRadius: BorderRadius.circular(10), + )), + ); + } +} + +class _TrailingIcon extends StatelessWidget { + final String asset; + final VoidCallback onPressed; + + const _TrailingIcon({this.asset, this.onPressed}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerRight, + width: 25, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: onPressed, + child: Image.asset( + asset, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart new file mode 100644 index 000000000..d264842e6 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart @@ -0,0 +1,217 @@ +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class IoniaPaymentStatusPage extends BasePage { + IoniaPaymentStatusPage(this.viewModel); + + final IoniaPaymentStatusViewModel viewModel; + + @override + Widget middle(BuildContext context) { + return Text( + S.of(context).generating_gift_card, + textAlign: TextAlign.center, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor)); + } + + @override + Widget body(BuildContext context) { + return _IoniaPaymentStatusPageBody(viewModel); + } +} + +class _IoniaPaymentStatusPageBody extends StatefulWidget { + _IoniaPaymentStatusPageBody(this.viewModel); + + final IoniaPaymentStatusViewModel viewModel; + + @override + _IoniaPaymentStatusPageBodyBodyState createState() => _IoniaPaymentStatusPageBodyBodyState(); +} + +class _IoniaPaymentStatusPageBodyBodyState extends State<_IoniaPaymentStatusPageBody> { + ReactionDisposer _onGiftCardReaction; + + @override + void initState() { + if (widget.viewModel.giftCard != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context) + .pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [widget.viewModel.giftCard]); + }); + } + + _onGiftCardReaction = reaction((_) => widget.viewModel.giftCard, (IoniaGiftCard giftCard) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context) + .pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [giftCard]); + }); + }); + + super.initState(); + } + + @override + void dispose() { + _onGiftCardReaction?.reaction?.dispose(); + widget.viewModel.timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.only(right: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.green), + height: 10, + width: 10)), + Text( + S.of(context).awaiting_payment_confirmation, + style: textLargeSemiBold( + color: Theme.of(context).primaryTextTheme.title.color)) + ]), + SizedBox(height: 40), + Row(children: [ + SizedBox(width: 20), + Expanded(child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ...widget.viewModel + .committedInfo + .transactions + .map((transaction) => buildDescriptionTileWithCopy(context, S.of(context).transaction_details_transaction_id, transaction.id)), + Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id), + Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId), + ])) + ]), + SizedBox(height: 40), + Observer(builder: (_) { + if (widget.viewModel.giftCard != null) { + return Container( + padding: EdgeInsets.only(top: 40), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 10,), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.green), + height: 10, + width: 10)), + Text( + S.of(context).gift_card_is_generated, + style: textLargeSemiBold( + color: Theme.of(context).primaryTextTheme.title.color)) + ])); + } + + return Row(children: [ + Padding( + padding: EdgeInsets.only(right: 10), + child: Observer(builder: (_) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.viewModel.giftCard == null ? Colors.grey : Colors.green), + height: 10, + width: 10); + })), + Text( + S.of(context).generating_gift_card, + style: textLargeSemiBold( + color: Theme.of(context).primaryTextTheme.title.color))]); + }), + ], + ), + bottomSection: Padding( + padding: EdgeInsets.only(bottom: 12), + child: Column(children: [ + Container( + padding: EdgeInsets.only(left: 40, right: 40, bottom: 20), + child: Text( + S.of(context).proceed_after_one_minute, + style: textMedium( + color: Theme.of(context).primaryTextTheme.title.color, + ).copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + )), + Observer(builder: (_) { + if (widget.viewModel.giftCard != null) { + return PrimaryButton( + onPressed: () => Navigator.of(context) + .pushReplacementNamed( + Routes.ioniaGiftCardDetailPage, + arguments: [widget.viewModel.giftCard]), + text: S.of(context).open_gift_card, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white); + } + + return PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed(Routes.support), + text: S.of(context).contact_support, + color: Theme.of(context).accentTextTheme.caption.color, + textColor: Theme.of(context).primaryTextTheme.title.color); + }) + ]) + ), + ); + } + + Widget buildDescriptionTile(BuildContext context, String title, String subtitle, VoidCallback onTap) { + return GestureDetector( + onTap: () => onTap(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textXSmall( + color: Theme.of(context).primaryTextTheme.overline.color, + ), + ), + SizedBox(height: 8), + Text( + subtitle, + style: textMedium( + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + ], + )); + } + + Widget buildDescriptionTileWithCopy(BuildContext context, String title, String subtitle) { + return buildDescriptionTile(context, title, subtitle, () { + Clipboard.setData(ClipboardData(text: subtitle)); + showBar(context, + S.of(context).transaction_details_copied(title)); + }); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/ionia.dart b/lib/src/screens/ionia/ionia.dart new file mode 100644 index 000000000..bdc2065a9 --- /dev/null +++ b/lib/src/screens/ionia/ionia.dart @@ -0,0 +1,9 @@ +export 'auth/ionia_welcome_page.dart'; +export 'auth/ionia_create_account_page.dart'; +export 'auth/ionia_login_page.dart'; +export 'auth/ionia_verify_otp_page.dart'; +export 'cards/ionia_activate_debit_card_page.dart'; +export 'cards/ionia_buy_card_detail_page.dart'; +export 'cards/ionia_manage_cards_page.dart'; +export 'cards/ionia_debit_card_page.dart'; +export 'cards/ionia_buy_gift_card.dart'; diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart new file mode 100644 index 000000000..ae3554331 --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_item.dart @@ -0,0 +1,139 @@ +import 'package:cake_wallet/src/widgets/discount_badge.dart'; +import 'package:flutter/material.dart'; + +class CardItem extends StatelessWidget { + CardItem({ + @required this.title, + @required this.subTitle, + @required this.backgroundColor, + @required this.titleColor, + @required this.subtitleColor, + this.discountBackground, + this.onTap, + this.logoUrl, + this.discount, + }); + + final VoidCallback onTap; + final String title; + final String subTitle; + final String logoUrl; + final double discount; + final Color backgroundColor; + final Color titleColor; + final Color subtitleColor; + final AssetImage discountBackground; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.20), + ), + ), + child: Row( + children: [ + if (logoUrl != null) ...[ + ClipOval( + child: Image.network( + logoUrl, + width: 40.0, + height: 40.0, + fit: BoxFit.cover, + loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent loadingProgress) { + if (loadingProgress == null) { + return child; + } else { + return _PlaceholderContainer(text: 'Logo'); + } + }, + errorBuilder: (_, __, ___) => _PlaceholderContainer(text: '!'), + ), + ), + SizedBox(width: 5), + ], + Column( + crossAxisAlignment: (subTitle?.isEmpty ?? false) + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontSize: 20, + fontWeight: FontWeight.w900, + ), + ), + ), + if (subTitle?.isNotEmpty ?? false) + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + subTitle, + style: TextStyle( + color: subtitleColor, + fontWeight: FontWeight.w500, + fontFamily: 'Lato')), + ) + ], + ), + ], + ), + ), + if (discount != 0.0) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: DiscountBadge( + percentage: discount, + discountBackground: discountBackground, + ), + ), + ), + ], + ), + ); + } +} + +class _PlaceholderContainer extends StatelessWidget { + const _PlaceholderContainer({@required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + height: 42, + width: 42, + child: Center( + child: Text( + text, + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w900, + ), + ), + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/card_menu.dart b/lib/src/screens/ionia/widgets/card_menu.dart new file mode 100644 index 000000000..9212c0448 --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_menu.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CardMenu extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Container( + + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/widgets/confirm_modal.dart b/lib/src/screens/ionia/widgets/confirm_modal.dart new file mode 100644 index 000000000..33a89a9b0 --- /dev/null +++ b/lib/src/screens/ionia/widgets/confirm_modal.dart @@ -0,0 +1,148 @@ +import 'dart:ui'; + +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/material.dart'; + +class IoniaConfirmModal extends StatelessWidget { + IoniaConfirmModal({ + @required this.alertTitle, + @required this.alertContent, + @required this.leftButtonText, + @required this.rightButtonText, + @required this.actionLeftButton, + @required this.actionRightButton, + this.leftActionColor, + this.rightActionColor, + this.hideActions = false, + }); + + final String alertTitle; + final Widget alertContent; + final String leftButtonText; + final String rightButtonText; + final VoidCallback actionLeftButton; + final VoidCallback actionRightButton; + final Color leftActionColor; + final Color rightActionColor; + final bool hideActions; + + Widget actionButtons(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + IoniaActionButton( + buttonText: leftButtonText, + action: actionLeftButton, + backgoundColor: leftActionColor, + ), + Container( + width: 1, + height: 52, + color: Theme.of(context).dividerColor, + ), + IoniaActionButton( + buttonText: rightButtonText, + action: actionRightButton, + backgoundColor: rightActionColor, + ), + ], + ); + } + + Widget title(BuildContext context) { + return Text( + alertTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.transparent, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), + child: Container( + decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), + child: Center( + child: GestureDetector( + onTap: () => null, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + width: 327, + color: Theme.of(context).accentTextTheme.title.decorationColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 20, 24, 0), + child: title(context), + ), + Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + alertContent, + actionButtons(context), + ], + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class IoniaActionButton extends StatelessWidget { + const IoniaActionButton({ + @required this.buttonText, + @required this.action, + this.backgoundColor, + }); + + final String buttonText; + final VoidCallback action; + final Color backgoundColor; + + @override + Widget build(BuildContext context) { + return Flexible( + child: Container( + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: backgoundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: action, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + buttonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: backgoundColor != null ? Colors.white : Theme.of(context).primaryTextTheme.body1.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )); + } +} diff --git a/lib/src/screens/ionia/widgets/ionia_alert_model.dart b/lib/src/screens/ionia/widgets/ionia_alert_model.dart new file mode 100644 index 000000000..abe442d8e --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_alert_model.dart @@ -0,0 +1,86 @@ +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; + +class IoniaAlertModal extends StatelessWidget { + const IoniaAlertModal({ + Key key, + @required this.title, + @required this.content, + @required this.actionTitle, + this.heightFactor = 0.4, + this.showCloseButton = true, + }) : super(key: key); + + final String title; + final Widget content; + final String actionTitle; + final bool showCloseButton; + final double heightFactor; + + @override + Widget build(BuildContext context) { + return AlertBackground( + child: Material( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Spacer(), + Container( + padding: EdgeInsets.only(top: 24, left: 24, right: 24), + margin: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + children: [ + if (title.isNotEmpty) + Text( + title, + style: textLargeSemiBold( + color: Theme.of(context).textTheme.body1.color, + ), + ), + Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * heightFactor), + child: ListView( + children: [ + content, + SizedBox(height: 35), + ], + ), + ), + PrimaryButton( + onPressed: () => Navigator.pop(context), + text: actionTitle, + color: Theme.of(context).accentTextTheme.caption.color, + textColor: Theme.of(context).primaryTextTheme.title.color, + ), + SizedBox(height: 21), + ], + ), + ), + Spacer(), + if(showCloseButton) + InkWell( + onTap: () => Navigator.pop(context), + child: Container( + margin: EdgeInsets.only(bottom: 40), + child: CircleAvatar( + child: Icon( + Icons.close, + color: Colors.black, + ), + backgroundColor: Colors.white, + ), + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart new file mode 100644 index 000000000..242b19e51 --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart @@ -0,0 +1,134 @@ +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class IoniaFilterModal extends StatelessWidget { + IoniaFilterModal({ + @required this.filterViewModel, + @required this.selectedCategories, + }) { + filterViewModel.setSelectedCategories(this.selectedCategories); + } + + final IoniaFilterViewModel filterViewModel; + final List selectedCategories; + + @override + Widget build(BuildContext context) { + final searchIcon = Padding( + padding: EdgeInsets.all(10), + child: Image.asset( + 'assets/images/mini_search_icon.png', + color: Theme.of(context).accentColor, + ), + ); + return Scaffold( + resizeToAvoidBottomInset: false, + body: AlertBackground( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 10), + Container( + padding: EdgeInsets.only(top: 24, bottom: 20), + margin: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + children: [ + SizedBox( + height: 40, + child: Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: TextField( + onChanged: filterViewModel.onSearchFilter, + style: textMedium( + color: Theme.of(context).primaryTextTheme.title.color, + ), + decoration: InputDecoration( + filled: true, + prefixIcon: searchIcon, + hintText: S.of(context).search_category, + contentPadding: EdgeInsets.only(bottom: 5), + fillColor: Theme.of(context).textTheme.subhead.backgroundColor, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ), + SizedBox(height: 10), + Divider(thickness: 2), + SizedBox(height: 24), + Observer(builder: (_) { + return ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: filterViewModel.ioniaCategories.length, + itemBuilder: (_, index) { + final category = filterViewModel.ioniaCategories[index]; + return Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: InkWell( + onTap: () => filterViewModel.selectFilter(category), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + category.iconPath, + color: Theme.of(context).primaryTextTheme.title.color, + ), + SizedBox(width: 10), + Text(category.title, + style: textSmall( + color: Theme.of(context).primaryTextTheme.title.color, + ).copyWith(fontWeight: FontWeight.w500)), + ], + ), + Observer(builder: (_) { + final value = filterViewModel.selectedIndices; + return RoundedCheckbox( + value: value.contains(category.index), + ); + }), + ], + ), + ), + ); + }, + ); + }), + ], + ), + ), + InkWell( + onTap: () => Navigator.pop(context, filterViewModel.selectedCategories), + child: Container( + margin: EdgeInsets.only(bottom: 40), + child: CircleAvatar( + child: Icon( + Icons.close, + color: Colors.black, + ), + backgroundColor: Colors.white, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/ionia_tile.dart b/lib/src/screens/ionia/widgets/ionia_tile.dart new file mode 100644 index 000000000..1e066312c --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_tile.dart @@ -0,0 +1,44 @@ +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; + +class IoniaTile extends StatelessWidget { + const IoniaTile({ + Key key, + @required this.title, + @required this.subTitle, + this.onTap, + }) : super(key: key); + + final VoidCallback onTap; + final String title; + final String subTitle; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onTap(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textXSmall( + color: Theme.of(context).primaryTextTheme.overline.color, + ), + ), + SizedBox(height: 8), + Text( + subTitle, + style: textMediumBold( + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + ], + ) + ], + )); + } +} diff --git a/lib/src/screens/ionia/widgets/rounded_checkbox.dart b/lib/src/screens/ionia/widgets/rounded_checkbox.dart new file mode 100644 index 000000000..095ad08b5 --- /dev/null +++ b/lib/src/screens/ionia/widgets/rounded_checkbox.dart @@ -0,0 +1,27 @@ +import 'dart:ui'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class RoundedCheckbox extends StatelessWidget { + RoundedCheckbox({Key key, @required this.value}) : super(key: key); + + final bool value; + + @override + Widget build(BuildContext context) { + return value + ? Container( + height: 20.0, + width: 20.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(50.0)), + color: Theme.of(context).accentTextTheme.body2.color, + ), + child: Icon( + Icons.check, + color: Theme.of(context).backgroundColor, + size: 14.0, + )) + : Offstage(); + } +} diff --git a/lib/src/screens/ionia/widgets/text_icon_button.dart b/lib/src/screens/ionia/widgets/text_icon_button.dart new file mode 100644 index 000000000..16606e65d --- /dev/null +++ b/lib/src/screens/ionia/widgets/text_icon_button.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; + +class TextIconButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + const TextIconButton({ + Key key, + this.label, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return + InkWell( + onTap: onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: textMediumSemiBold( + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + Icon( + Icons.chevron_right_rounded, + color: Theme.of(context).primaryTextTheme.title.color, + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index c2831675e..f3c66f275 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -10,7 +10,10 @@ class AlertWithTwoActions extends BaseAlertDialog { @required this.rightButtonText, @required this.actionLeftButton, @required this.actionRightButton, - this.alertBarrierDismissible = true + this.alertBarrierDismissible = true, + this.isDividerExist = false, + this.leftActionColor, + this.rightActionColor, }); final String alertTitle; @@ -20,6 +23,9 @@ class AlertWithTwoActions extends BaseAlertDialog { final VoidCallback actionLeftButton; final VoidCallback actionRightButton; final bool alertBarrierDismissible; + final Color leftActionColor; + final Color rightActionColor; + final bool isDividerExist; @override String get titleText => alertTitle; @@ -35,4 +41,10 @@ class AlertWithTwoActions extends BaseAlertDialog { VoidCallback get actionRight => actionRightButton; @override bool get barrierDismissible => alertBarrierDismissible; -} \ No newline at end of file + @override + Color get leftButtonColor => leftActionColor; + @override + Color get rightButtonColor => rightActionColor; + @override + bool get isDividerExists => isDividerExist; +} diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 19ff62ce2..d0aaace2f 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -46,30 +46,28 @@ class BaseAlertDialog extends StatelessWidget { children: [ Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body2.decorationColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionLeft, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - leftActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body2 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body2.decorationColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionLeft, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + leftActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body2.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), Container( width: 1, height: 52, @@ -77,30 +75,28 @@ class BaseAlertDialog extends StatelessWidget { ), Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body1.backgroundColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionRight, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - rightActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body1 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body1.backgroundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionRight, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + rightActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body1.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), ], ); } @@ -108,9 +104,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => barrierDismissible - ? Navigator.of(context).pop() - : null, + onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, child: BackdropFilter( @@ -136,14 +130,14 @@ class BaseAlertDialog extends StatelessWidget { child: title(context), ), isDividerExists - ? Padding( - padding: EdgeInsets.only(top: 16, bottom: 8), - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ) - : Offstage(), + ? Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ) + : Offstage(), Padding( padding: EdgeInsets.fromLTRB(24, 8, 24, 32), child: content(context), @@ -166,4 +160,4 @@ class BaseAlertDialog extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/cake_scrollbar.dart b/lib/src/widgets/cake_scrollbar.dart index 6ccf391c6..6a0cb2e14 100644 --- a/lib/src/widgets/cake_scrollbar.dart +++ b/lib/src/widgets/cake_scrollbar.dart @@ -5,13 +5,19 @@ class CakeScrollbar extends StatelessWidget { @required this.backgroundHeight, @required this.thumbHeight, @required this.fromTop, - this.rightOffset = 6 + this.rightOffset = 6, + this.backgroundColor, + this.thumbColor, + this.width = 6, }); final double backgroundHeight; final double thumbHeight; final double fromTop; + final double width; final double rightOffset; + final Color backgroundColor; + final Color thumbColor; @override Widget build(BuildContext context) { @@ -19,11 +25,10 @@ class CakeScrollbar extends StatelessWidget { right: rightOffset, child: Container( height: backgroundHeight, - width: 6, + width: width, decoration: BoxDecoration( - color: Theme.of(context).textTheme.body1.decorationColor, - borderRadius: BorderRadius.all(Radius.circular(3)) - ), + color: backgroundColor ?? Theme.of(context).textTheme.body1.decorationColor, + borderRadius: BorderRadius.all(Radius.circular(3))), child: Stack( children: [ AnimatedPositioned( @@ -31,16 +36,14 @@ class CakeScrollbar extends StatelessWidget { top: fromTop, child: Container( height: thumbHeight, - width: 6.0, + width: width, decoration: BoxDecoration( - color: Theme.of(context).textTheme.body1.color, - borderRadius: BorderRadius.all(Radius.circular(3)) - ), + color: thumbColor ?? Theme.of(context).textTheme.body1.color, + borderRadius: BorderRadius.all(Radius.circular(3))), ), ) ], ), - ) - ); + )); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/discount_badge.dart b/lib/src/widgets/discount_badge.dart new file mode 100644 index 000000000..1cbbf3e89 --- /dev/null +++ b/lib/src/widgets/discount_badge.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class DiscountBadge extends StatelessWidget { + const DiscountBadge({ + Key key, + @required this.percentage, + this.discountBackground, + }) : super(key: key); + + final double percentage; + final AssetImage discountBackground; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + S.of(context).discount(percentage.toStringAsFixed(2)), + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + ), + ), + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fill, + image: discountBackground ?? AssetImage('assets/images/badge_discount.png'), + ), + ), + ); + } +} diff --git a/lib/src/widgets/market_place_item.dart b/lib/src/widgets/market_place_item.dart new file mode 100644 index 000000000..0112ea00d --- /dev/null +++ b/lib/src/widgets/market_place_item.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class MarketPlaceItem extends StatelessWidget { + + + MarketPlaceItem({ + @required this.onTap, + @required this.title, + @required this.subTitle, + }); + + final VoidCallback onTap; + final String title; + final String subTitle; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(20), + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).textTheme.title.backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.20), + ), + ), + child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + SizedBox(height: 5), + Text( + subTitle, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + fontWeight: FontWeight.w500, + fontFamily: 'Lato'), + ) + ], + ), + ), + ], + ), + ); + } +} + diff --git a/lib/typography.dart b/lib/typography.dart new file mode 100644 index 000000000..08491e2cb --- /dev/null +++ b/lib/typography.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +const latoFont = "Lato"; + +TextStyle textXxSmall({Color color}) => _cakeRegular(10, color); + +TextStyle textXxSmallSemiBold({Color color}) => _cakeSemiBold(10, color); + +TextStyle textXSmall({Color color}) => _cakeRegular(12, color); + +TextStyle textXSmallSemiBold({Color color}) => _cakeSemiBold(12, color); + +TextStyle textSmall({Color color}) => _cakeRegular(14, color); + +TextStyle textSmallSemiBold({Color color}) => _cakeSemiBold(14, color); + +TextStyle textMedium({Color color}) => _cakeRegular(16, color); + +TextStyle textMediumBold({Color color}) => _cakeBold(16, color); + +TextStyle textMediumSemiBold({Color color}) => _cakeSemiBold(22, color); + +TextStyle textLarge({Color color}) => _cakeRegular(18, color); + +TextStyle textLargeSemiBold({Color color}) => _cakeSemiBold(24, color); + +TextStyle textXLarge({Color color}) => _cakeRegular(32, color); + +TextStyle textXLargeSemiBold({Color color}) => _cakeSemiBold(32, color); + +TextStyle _cakeRegular(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.normal, + color: color, + ); + +TextStyle _cakeBold(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.w900, + color: color, + ); + +TextStyle _cakeSemiBold(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.w700, + color: color, + ); + +TextStyle _textStyle({ + @required double size, + @required FontWeight fontWeight, + Color color, +}) => + TextStyle( + fontFamily: latoFont, + fontSize: size, + fontWeight: fontWeight, + color: color ?? Colors.white, + ); diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart new file mode 100644 index 000000000..a4875ec61 --- /dev/null +++ b/lib/view_model/ionia/ionia_account_view_model.dart @@ -0,0 +1,44 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; + +part 'ionia_account_view_model.g.dart'; + +class IoniaAccountViewModel = IoniaAccountViewModelBase with _$IoniaAccountViewModel; + +abstract class IoniaAccountViewModelBase with Store { + IoniaAccountViewModelBase({this.ioniaService}) { + email = ''; + giftCards = []; + ioniaService.getUserEmail().then((email) => this.email = email); + updateUserGiftCards(); + } + + final IoniaService ioniaService; + + @observable + String email; + + @observable + List giftCards; + + @computed + int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length; + + @computed + List get activeMechs => giftCards.where((giftCard) => !giftCard.isEmpty).toList(); + + @computed + List get redeemedMerchs => giftCards.where((giftCard) => giftCard.isEmpty).toList(); + + @action + void logout() { + ioniaService.logout(); + } + + @action + Future updateUserGiftCards() async { + giftCards = await ioniaService.getCurrentUserGiftCardSummaries(); + } +} diff --git a/lib/view_model/ionia/ionia_auth_view_model.dart b/lib/view_model/ionia/ionia_auth_view_model.dart new file mode 100644 index 000000000..5e662b462 --- /dev/null +++ b/lib/view_model/ionia/ionia_auth_view_model.dart @@ -0,0 +1,67 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_auth_view_model.g.dart'; + +class IoniaAuthViewModel = IoniaAuthViewModelBase with _$IoniaAuthViewModel; + +abstract class IoniaAuthViewModelBase with Store { + + IoniaAuthViewModelBase({this.ioniaService}): + createUserState = IoniaInitialCreateState(), + signInState = IoniaInitialCreateState(), + otpState = IoniaOtpSendDisabled(); + + final IoniaService ioniaService; + + @observable + IoniaCreateAccountState createUserState; + + @observable + IoniaCreateAccountState signInState; + + @observable + IoniaOtpState otpState; + + @observable + String email; + + @observable + String otp; + + @action + Future verifyEmail(String code) async { + try { + otpState = IoniaOtpValidating(); + await ioniaService.verifyEmail(code); + otpState = IoniaOtpSuccess(); + } catch (_) { + otpState = IoniaOtpFailure(error: 'Invalid OTP. Try again'); + } + } + + @action + Future createUser(String email) async { + try { + createUserState = IoniaCreateStateLoading(); + await ioniaService.createUser(email); + createUserState = IoniaCreateStateSuccess(); + } catch (e) { + createUserState = IoniaCreateStateFailure(error: e.toString()); + } + } + + + @action + Future signIn(String email) async { + try { + signInState = IoniaCreateStateLoading(); + await ioniaService.signIn(email); + signInState = IoniaCreateStateSuccess(); + } catch (e) { + signInState = IoniaCreateStateFailure(error: e.toString()); + } + } + +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_buy_card_view_model.dart b/lib/view_model/ionia/ionia_buy_card_view_model.dart new file mode 100644 index 000000000..b6314be80 --- /dev/null +++ b/lib/view_model/ionia/ionia_buy_card_view_model.dart @@ -0,0 +1,31 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_buy_card_view_model.g.dart'; + +class IoniaBuyCardViewModel = IoniaBuyCardViewModelBase with _$IoniaBuyCardViewModel; + +abstract class IoniaBuyCardViewModelBase with Store { + IoniaBuyCardViewModelBase({this.ioniaMerchant}) { + isEnablePurchase = false; + amount = 0; + } + + final IoniaMerchant ioniaMerchant; + + @observable + double amount; + + @observable + bool isEnablePurchase; + + @action + void onAmountChanged(String input) { + if (input.isEmpty) return; + amount = double.parse(input.replaceAll(',', '.')); + final min = ioniaMerchant.minimumCardPurchase; + final max = ioniaMerchant.maximumCardPurchase; + + isEnablePurchase = amount >= min && amount <= max; + } +} diff --git a/lib/view_model/ionia/ionia_filter_view_model.dart b/lib/view_model/ionia/ionia_filter_view_model.dart new file mode 100644 index 000000000..43d2790e0 --- /dev/null +++ b/lib/view_model/ionia/ionia_filter_view_model.dart @@ -0,0 +1,58 @@ +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_filter_view_model.g.dart'; + +class IoniaFilterViewModel = IoniaFilterViewModelBase with _$IoniaFilterViewModel; + +abstract class IoniaFilterViewModelBase with Store { + IoniaFilterViewModelBase() { + selectedIndices = ObservableList(); + ioniaCategories = IoniaCategory.allCategories; + } + + List get selectedCategories => ioniaCategories.where(_isSelected).toList(); + + @observable + ObservableList selectedIndices; + + @observable + List ioniaCategories; + + @action + void selectFilter(IoniaCategory ioniaCategory) { + if (ioniaCategory == IoniaCategory.all && !selectedIndices.contains(0)) { + selectedIndices.clear(); + selectedIndices.add(0); + return; + } + if (selectedIndices.contains(ioniaCategory.index) && ioniaCategory.index != 0) { + selectedIndices.remove(ioniaCategory.index); + return; + } + selectedIndices.add(ioniaCategory.index); + selectedIndices.remove(0); + } + + @action + void onSearchFilter(String text) { + if (text.isEmpty) { + ioniaCategories = IoniaCategory.allCategories; + } else { + ioniaCategories = IoniaCategory.allCategories + .where( + (e) => e.title.toLowerCase().contains(text.toLowerCase()), + ) + .toList(); + } + } + + @action + void setSelectedCategories(List selectedCategories) { + selectedIndices = ObservableList.of(selectedCategories.map((e) => e.index)); + } + + bool _isSelected(IoniaCategory ioniaCategory) { + return selectedIndices.contains(ioniaCategory.index); + } +} diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart new file mode 100644 index 000000000..e6138bb53 --- /dev/null +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_gift_card_details_view_model.g.dart'; + +class IoniaGiftCardDetailsViewModel = IoniaGiftCardDetailsViewModelBase with _$IoniaGiftCardDetailsViewModel; + +abstract class IoniaGiftCardDetailsViewModelBase with Store { + + IoniaGiftCardDetailsViewModelBase({this.ioniaService, this.giftCard}) { + redeemState = InitialExecutionState(); + } + + final IoniaService ioniaService; + + @observable + IoniaGiftCard giftCard; + + @observable + ExecutionState redeemState; + + @action + Future redeem() async { + try { + redeemState = IsExecutingState(); + await ioniaService.redeem(giftCard); + giftCard = await ioniaService.getGiftCard(id: giftCard.id); + redeemState = ExecutedSuccessfullyState(); + } catch(e) { + redeemState = FailureState(e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart new file mode 100644 index 000000000..f20000a6d --- /dev/null +++ b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart @@ -0,0 +1,103 @@ +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +part 'ionia_gift_cards_list_view_model.g.dart'; + +class IoniaGiftCardsListViewModel = IoniaGiftCardsListViewModelBase with _$IoniaGiftCardsListViewModel; + +abstract class IoniaGiftCardsListViewModelBase with Store { + IoniaGiftCardsListViewModelBase({ + @required this.ioniaService, + }) : + cardState = IoniaNoCardState(), + ioniaMerchants = [], + scrollOffsetFromTop = 0.0 { + selectedFilters = []; + _getAuthStatus().then((value) => isLoggedIn = value); + + _getMerchants(); + } + + final IoniaService ioniaService; + + List ioniaMerchantList; + + String searchString; + + List selectedFilters; + + @observable + double scrollOffsetFromTop; + + @observable + IoniaCreateCardState createCardState; + + @observable + IoniaFetchCardState cardState; + + @observable + List ioniaMerchants; + + @observable + bool isLoggedIn; + + Future _getAuthStatus() async { + return await ioniaService.isLogined(); + } + + @action + Future createCard() async { + createCardState = IoniaCreateCardLoading(); + try { + final card = await ioniaService.createCard(); + createCardState = IoniaCreateCardSuccess(); + return card; + } on Exception catch (e) { + createCardState = IoniaCreateCardFailure(error: e.toString()); + } + return null; + } + + @action + void searchMerchant(String text) { + if (text.isEmpty) { + ioniaMerchants = ioniaMerchantList; + return; + } + searchString = text; + ioniaService.getMerchantsByFilter(search: searchString).then((value) { + ioniaMerchants = value; + }); + } + + Future _getCard() async { + cardState = IoniaFetchingCard(); + try { + final card = await ioniaService.getCard(); + + cardState = IoniaCardSuccess(card: card); + } catch (_) { + cardState = IoniaFetchCardFailure(); + } + } + + void _getMerchants() { + ioniaService.getMerchantsByFilter(categories: selectedFilters).then((value) { + ioniaMerchants = ioniaMerchantList = value; + }); + } + + @action + void setSelectedFilter(List filters) { + selectedFilters = filters; + _getMerchants(); + } + + void setScrollOffsetFromTop(double scrollOffset) { + scrollOffsetFromTop = scrollOffset; + } +} diff --git a/lib/view_model/ionia/ionia_payment_status_view_model.dart b/lib/view_model/ionia/ionia_payment_status_view_model.dart new file mode 100644 index 000000000..187e24856 --- /dev/null +++ b/lib/view_model/ionia/ionia_payment_status_view_model.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:mobx/mobx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; + +part 'ionia_payment_status_view_model.g.dart'; + +class IoniaPaymentStatusViewModel = IoniaPaymentStatusViewModelBase with _$IoniaPaymentStatusViewModel; + +abstract class IoniaPaymentStatusViewModelBase with Store { + IoniaPaymentStatusViewModelBase( + this.ioniaService,{ + @required this.paymentInfo, + @required this.committedInfo}) { + _timer = Timer.periodic(updateTime, (timer) async { + await updatePaymentStatus(); + + if (giftCard != null) { + timer?.cancel(); + } + }); + } + + static const updateTime = Duration(seconds: 3); + + final IoniaService ioniaService; + final IoniaAnyPayPaymentInfo paymentInfo; + final AnyPayPaymentCommittedInfo committedInfo; + + @observable + IoniaGiftCard giftCard; + + @observable + String error; + + Timer get timer => _timer; + + Timer _timer; + + @action + Future updatePaymentStatus() async { + try { + final giftCardId = await ioniaService.getPaymentStatus( + orderId: paymentInfo.ioniaOrder.id, + paymentId: paymentInfo.ioniaOrder.paymentId); + + if (giftCardId != null) { + giftCard = await ioniaService.getGiftCard(id: giftCardId); + } + + } catch (e) { + error = e.toString(); + } + } +} diff --git a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart new file mode 100644 index 000000000..b8c8eaa96 --- /dev/null +++ b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart @@ -0,0 +1,94 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/anypay/any_pay_payment.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ionia/ionia_anypay.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; + +part 'ionia_purchase_merch_view_model.g.dart'; + +class IoniaMerchPurchaseViewModel = IoniaMerchPurchaseViewModelBase with _$IoniaMerchPurchaseViewModel; + +abstract class IoniaMerchPurchaseViewModelBase with Store { + IoniaMerchPurchaseViewModelBase({ + @required this.ioniaAnyPayService, + @required this.amount, + @required this.ioniaMerchant, + }) { + tipAmount = 0.0; + percentage = 0.0; + tips = [ + IoniaTip(percentage: 0, originalAmount: amount), + IoniaTip(percentage: 15, originalAmount: amount), + IoniaTip(percentage: 18, originalAmount: amount), + IoniaTip(percentage: 20, originalAmount: amount), + ]; + selectedTip = tips.first; + } + + final double amount; + + List tips; + + @observable + IoniaTip selectedTip; + + final IoniaMerchant ioniaMerchant; + + final IoniaAnyPay ioniaAnyPayService; + + IoniaAnyPayPaymentInfo paymentInfo; + + AnyPayPayment get invoice => paymentInfo?.anyPayPayment; + + AnyPayPaymentCommittedInfo committedInfo; + + @observable + ExecutionState invoiceCreationState; + + @observable + ExecutionState invoiceCommittingState; + + @observable + double percentage; + + @computed + double get giftCardAmount => double.parse((amount + tipAmount).toStringAsFixed(2)); + + @computed + double get billAmount => double.parse((giftCardAmount * (1 - (ioniaMerchant.discount / 100))).toStringAsFixed(2)); + + @observable + double tipAmount; + + @action + void addTip(IoniaTip tip) { + tipAmount = tip.additionalAmount; + selectedTip = tip; + } + + @action + Future createInvoice() async { + try { + invoiceCreationState = IsExecutingState(); + paymentInfo = await ioniaAnyPayService.purchase(merchId: ioniaMerchant.id.toString(), amount: giftCardAmount); + invoiceCreationState = ExecutedSuccessfullyState(); + } catch (e) { + invoiceCreationState = FailureState(e.toString()); + } + } + + @action + Future commitPaymentInvoice() async { + try { + invoiceCommittingState = IsExecutingState(); + committedInfo = await ioniaAnyPayService.commitInvoice(invoice); + invoiceCommittingState = ExecutedSuccessfullyState(payload: committedInfo); + } catch (e) { + invoiceCommittingState = FailureState(e.toString()); + } + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index d82dd979f..b615c00c4 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -222,11 +222,11 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: final priority = _settingsStore.priority[_wallet.type]; - return bitcoin.createBitcoinTransactionCredentials(outputs, priority); + return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.litecoin: final priority = _settingsStore.priority[_wallet.type]; - return bitcoin.createBitcoinTransactionCredentials(outputs, priority); + return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.monero: final priority = _settingsStore.priority[_wallet.type]; diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 6d88ae671..d9687bc36 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -534,5 +534,103 @@ "search_currency": "Währung suchen", "new_template" : "neue Vorlage", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", - "wallet_name_exists": "Wallet mit diesem Namen existiert bereits" + "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", + "market_place": "Marktplatz", + "cake_pay_title": "Cake Pay-Geschenkkarten", + "cake_pay_subtitle": "Geschenkkarten kaufen und sofort einlösen", + "about_cake_pay": "Mit Cake Pay können Sie ganz einfach Geschenkkarten mit virtuellen Vermögenswerten kaufen, die Sie sofort bei über 150.000 Händlern in den Vereinigten Staaten ausgeben können.", + "cake_pay_account_note": "Erstellen Sie ein Konto, um die verfügbaren Karten zu sehen. Einige sind sogar mit Rabatt erhältlich!", + "already_have_account": "Sie haben bereits ein Konto?", + "create_account": "Konto erstellen", + "privacy_policy": "Datenschutzrichtlinie", + "welcome_to_cakepay": "Willkommen bei Cake Pay!", + "sign_up": "Anmelden", + "forgot_password": "Passwort vergessen", + "reset_password": "Passwort zurücksetzen", + "gift_cards": "Geschenkkarten", + "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", + "no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben", + "how_to_use_card": "Wie man diese Karte benutzt", + "purchase_gift_card": "Geschenkkarte kaufen", + "verification": "Verifizierung", + "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", + "dont_get_code": "Kein Code?", + "resend_code": "Bitte erneut senden", + "debit_card": "Debitkarte", + "cakepay_prepaid_card": "CakePay-Prepaid-Debitkarte", + "no_id_needed": "Keine ID erforderlich!", + "frequently_asked_questions": "Häufig gestellte Fragen", + "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", + "cardholder_agreement": "Karteninhabervertrag", + "e_sign_consent": "E-Sign-Zustimmung", + "agree_and_continue": "Zustimmen & fortfahren", + "email_address": "E-Mail-Adresse", + "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", + "und": "und", + "enter_code": "Code eingeben", + "congratulations": "Glückwunsch!", + "you_now_have_debit_card": "Sie haben jetzt eine Debitkarte", + "min_amount": "Min: ${value}", + "max_amount": "Max: ${value}", + "enter_amount": "Betrag eingeben", + "billing_address_info": "Wenn Sie nach einer Rechnungsadresse gefragt werden, geben Sie bitte Ihre Lieferadresse an", + "order_physical_card": "Physische Karte bestellen", + "add_value": "Wert hinzufügen", + "activate": "aktivieren", + "get_a": "Hole ein", + "digital_and_physical_card": "digitale en fysieke prepaid debetkaart", + "get_card_note": " die u kunt herladen met digitale valuta. Geen aanvullende informatie nodig!", + "signup_for_card_accept_terms": "Meld je aan voor de kaart en accepteer de voorwaarden.", + "add_fund_to_card": "Voeg prepaid tegoed toe aan de kaarten (tot ${value})", + "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.", + "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.", + "optionally_order_card": "Optioneel een fysieke kaart bestellen.", + "hide_details" : "Details verbergen", + "show_details" : "Toon details", + "upto": "tot ${value}", + "discount": "Bespaar ${value}%", + "gift_card_amount": "Bedrag cadeaubon", + "bill_amount": "Bill bedrag", + "you_pay": "U betaalt", + "tip": "Tip:", + "custom": "aangepast", + "by_cake_pay": "door Cake Pay", + "expires": "Verloopt", + "mm": "MM", + "yy": "JJ", + "online": "online", + "offline": "Offline", + "gift_card_number": "Cadeaukaartnummer", + "pin_number": "PIN-nummer", + "total_saving": "Totale besparingen", + "last_30_days": "Laatste 30 dagen", + "avg_savings": "Gem. besparingen", + "view_all": "Alles bekijken", + "active_cards": "Actieve kaarten", + "delete_account": "Account verwijderen", + "cards": "Kaarten", + "active": "Actief", + "redeemed": "Verzilverd", + "gift_card_balance_note": "Cadeaukaarten met een resterend saldo verschijnen hier", + "gift_card_redeemed_note": "Cadeaubonnen die je hebt ingewisseld, verschijnen hier", + "logout": "Uitloggen", + "add_tip": "Tip toevoegen", + "percentageOf": "van ${amount}", + "is_percentage": "is", + "search_category": "Zoek categorie", + "mark_as_redeemed": "Markeer als ingewisseld", + "more_options": "Meer opties", + "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", + "transaction_sent_notice": "Als het scherm na 1 minuut niet verder gaat, controleer dan een blokverkenner en je e-mail.", + "agree": "mee eens", + "in_store": "In winkel", + "generating_gift_card": "Cadeaubon genereren", + "payment_was_received": "Uw betaling is ontvangen.", + "proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.", + "order_id": "Bestell-ID", + "gift_card_is_generated": "Geschenkkarte wird generiert", + "open_gift_card": "Geschenkkarte öffnen", + "contact_support": "Support kontaktieren", + "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 639c97c97..c66f9a6e6 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -245,7 +245,7 @@ "settings_only_transactions" : "Only transactions", "settings_none" : "None", "settings_support" : "Support", - "settings_terms_and_conditions" : "Terms and conditions", + "settings_terms_and_conditions" : "Terms and Conditions", "pin_is_incorrect" : "PIN is incorrect", @@ -534,5 +534,103 @@ "search_currency": "Search currency", "new_template" : "New Template", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", - "wallet_name_exists": "Wallet with that name has already existed" + "wallet_name_exists": "Wallet with that name has already existed", + "market_place": "Marketplace", + "cake_pay_title": "Cake Pay Gift Cards", + "cake_pay_subtitle": "Buy gift cards and redeem instantly", + "about_cake_pay": "Cake Pay allows you to easily buy gift cards with virtual assets, spendable instantly at over 150,000 merchants in the United States.", + "cake_pay_account_note": "Make an account to see the available cards. Some are even available at a discount!", + "already_have_account": "Already have an account?", + "create_account": "Create Account", + "privacy_policy": "Privacy Policy", + "welcome_to_cakepay": "Welcome to Cake Pay!", + "sign_up": "Sign Up", + "forgot_password": "Forgot Password", + "reset_password": "Reset Password", + "gift_cards": "Gift Cards", + "setup_your_debit_card": "Set up your debit card", + "no_id_required": "No ID required. Top up and spend anywhere", + "how_to_use_card": "How to use this card", + "purchase_gift_card": "Purchase Gift Card", + "verification": "Verification", + "fill_code": "Please fill in the verification code provided to your email", + "dont_get_code": "Don't get code?", + "resend_code": "Please resend it", + "debit_card": "Debit Card", + "cakepay_prepaid_card": "CakePay Prepaid Debit Card", + "no_id_needed": "No ID needed!", + "frequently_asked_questions": "Frequently asked questions", + "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", + "please_reference_document": "Please reference the documents below for more information.", + "cardholder_agreement": "Cardholder Agreement", + "e_sign_consent": "E-Sign Consent", + "agree_and_continue": "Agree & Continue", + "email_address": "Email Address", + "agree_to": "By creating account you agree to the ", + "and": "and", + "enter_code": "Enter code", + "congratulations": "Congratulations!", + "you_now_have_debit_card": "You now have a debit card", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Enter Amount", + "billing_address_info": "If asked for a billing address, provide your shipping address", + "order_physical_card": "Order Physical Card", + "add_value": "Add value", + "activate": "Activate", + "get_a": "Get a ", + "digital_and_physical_card": " digital and physical prepaid debit card", + "get_card_note": " that you can reload with digital currencies. No additional information needed!", + "signup_for_card_accept_terms": "Sign up for the card and accept the terms.", + "add_fund_to_card": "Add prepaid funds to the cards (up to ${value})", + "use_card_info_two": "Funds are converted to USD when the held in the prepaid account, not in digital currencies.", + "use_card_info_three": "Use the digital card online or with contactless payment methods.", + "optionally_order_card": "Optionally order a physical card.", + "hide_details" : "Hide Details", + "show_details" : "Show Details", + "upto": "up to ${value}", + "discount": "Save ${value}%", + "gift_card_amount": "Gift Card Amount", + "bill_amount": "Bill amount", + "you_pay": "You pay", + "tip": "Tip:", + "custom": "custom", + "by_cake_pay": "by Cake Pay", + "expires": "Expires", + "mm": "MM", + "yy": "YY", + "online": "Online", + "offline": "Offline", + "gift_card_number": "Gift card number", + "pin_number": "PIN number", + "total_saving": "Total Savings", + "last_30_days": "Last 30 days", + "avg_savings": "Avg. savings", + "view_all": "View all", + "active_cards": "Active cards", + "delete_account": "Delete Account", + "cards": "Cards", + "active": "Active", + "redeemed": "Redeemed", + "gift_card_balance_note": "Gift cards with a balance remaining will appear here", + "gift_card_redeemed_note": "Gift cards you’ve redeemed will appear here", + "logout": "Logout", + "add_tip": "Add Tip", + "percentageOf": "of ${amount}", + "is_percentage": "is", + "search_category": "Search category", + "mark_as_redeemed": "Mark As Redeemed", + "more_options": "More Options", + "awaiting_payment_confirmation": "Awaiting payment confirmation", + "transaction_sent_notice": "If the screen doesn’t proceed after 1 minute, check a block explorer and your email.", + "agree": "Agree", + "in_store": "In Store", + "generating_gift_card": "Generating Gift Card", + "payment_was_received": "Your payment was received.", + "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", + "order_id": "Order ID", + "gift_card_is_generated": "Gift Card is generated", + "open_gift_card": "Open Gift Card", + "contact_support": "Contact Support", + "gift_cards_unavailable": "Gift cards are available to purchase only through Monero, Bitcoin, and Litecoin at this time" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index efe78c145..084658081 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -534,5 +534,103 @@ "search_currency": "Moneda de búsqueda", "new_template" : "Nueva plantilla", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", - "wallet_name_exists": "Wallet con ese nombre ya ha existido" + "wallet_name_exists": "Wallet con ese nombre ya ha existido", + "market_place": "Mercado", + "cake_pay_title": "Tarjetas de regalo Cake Pay", + "cake_pay_subtitle": "Compra tarjetas de regalo y canjéalas al instante", + "about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.", + "cake_pay_account_note": "Crea una cuenta para ver las tarjetas disponibles. ¡Algunas incluso están disponibles con descuento!", + "already_have_account": "¿Ya tienes una cuenta?", + "create_account": "Crear Cuenta", + "privacy_policy": "Política de privacidad", + "welcome_to_cakepay": "¡Bienvenido a Cake Pay!", + "sign_up": "Registrarse", + "forgot_password": "Olvidé mi contraseña", + "reset_password": "Restablecer contraseña", + "gift_cards": "Tarjetas de regalo", + "setup_your_debit_card": "Configura tu tarjeta de débito", + "no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar", + "how_to_use_card": "Cómo usar esta tarjeta", + "purchase_gift_card": "Comprar tarjeta de regalo", + "verification": "Verificación", + "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", + "dont_get_code": "¿No obtienes el código?", + "resend_code": "Por favor reenvíalo", + "debit_card": "Tarjeta de Débito", + "cakepay_prepaid_card": "Tarjeta de Débito Prepago CakePay", + "no_id_needed": "¡No se necesita identificación!", + "frequently_asked_questions": "Preguntas frecuentes", + "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", + "please_reference_document": "Consulte los documentos a continuación para obtener más información.", + "cardholder_agreement": "Acuerdo del titular de la tarjeta", + "e_sign_consent": "Consentimiento de firma electrónica", + "agree_and_continue": "Aceptar y continuar", + "email_address": "Dirección de correo electrónico", + "agree_to": "Al crear una cuenta, aceptas ", + "and": "y", + "enter_code": "Ingresar código", + "congratulations": "Felicidades!", + "you_now_have_debit_card": "Ahora tiene una tarjeta de débito", + "min_amount" : "Mínimo: ${value}", + "max_amount" : "Máx: ${value}", + "enter_amount": "Ingrese la cantidad", + "billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío", + "order_physical_card": "Pedir tarjeta física", + "add_value": "Añadir valor", + "activate": "Activar", + "get_a": "Obtener un", + "digital_and_physical_card": " tarjeta de débito prepago digital y física", + "get_card_note": " que puedes recargar con monedas digitales. ¡No se necesita información adicional!", + "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", + "add_fund_to_card": "Agregar fondos prepagos a las tarjetas (hasta ${value})", + "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", + "use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.", + "optionally_order_card": "Opcionalmente pide una tarjeta física.", + "hide_details" : "Ocultar detalles", + "show_details": "Mostrar detalles", + "upto": "hasta ${value}", + "discount": "Ahorra ${value}%", + "gift_card_amount": "Cantidad de la tarjeta de regalo", + "bill_amount": "Importe de la factura", + "you_pay": "Tú pagas", + "tip": "Consejo:", + "personalizado": "personalizado", + "by_cake_pay": "por Cake Pay", + "expires": "Caduca", + "mm": "mm", + "yy": "YY", + "online": "En línea", + "offline": "fuera de línea", + "gift_card_number": "Número de tarjeta de regalo", + "pin_number": "Número PIN", + "total_saving": "Ahorro Total", + "last_30_days": "Últimos 30 días", + "avg_savings": "Ahorro promedio", + "view_all": "Ver todo", + "active_cards": "Tarjetas activas", + "delete_account": "Eliminar cuenta", + "cards": "Cartas", + "active": "Activo", + "redeemed": "Redimido", + "gift_card_balance_note": "Las tarjetas de regalo con saldo restante aparecerán aquí", + "gift_card_redeemed_note": "Las tarjetas de regalo que hayas canjeado aparecerán aquí", + "logout": "Cerrar sesión", + "add_tip": "Agregar sugerencia", + "percentageOf": "de ${amount}", + "is_percentage": "es", + "search_category": "Categoría de búsqueda", + "mark_as_redeemed": "Marcar como canjeado", + "more_options": "Más Opciones", + "awaiting_payment_confirmation": "Esperando confirmación de pago", + "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", + "agree": "De acuerdo", + "in_store": "En la tienda", + "generating_gift_card": "Generando tarjeta de regalo", + "payment_was_received": "Su pago fue recibido.", + "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", + "order_id": "Identificación del pedido", + "gift_card_is_generated": "Se genera la tarjeta de regalo", + "open_gift_card": "Abrir tarjeta de regalo", + "contact_support": "Contactar con Soporte", + "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 01b12f791..9817e7bef 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -243,7 +243,7 @@ "settings_only_transactions" : "Seulement les transactions", "settings_none" : "Rien", "settings_support" : "Support", - "settings_terms_and_conditions" : "Termes et conditions", + "settings_terms_and_conditions" : "Termes et Conditions", "pin_is_incorrect" : "Le code PIN est incorrect", @@ -532,5 +532,103 @@ "search_currency": "Devise de recherche", "new_template" : "Nouveau Modèle", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", - "wallet_name_exists": "Le portefeuille portant ce nom existe déjà" + "wallet_name_exists": "Le portefeuille portant ce nom existe déjà", + "market_place": "Place de marché", + "cake_pay_title": "Cartes cadeaux Cake Pay", + "cake_pay_subtitle": "Achetez des cartes-cadeaux et échangez-les instantanément", + "about_cake_pay": "Cake Pay vous permet d'acheter facilement des cartes-cadeaux avec des actifs virtuels, utilisables instantanément chez plus de 150 000 marchands aux États-Unis.", + "cake_pay_account_note": "Créez un compte pour voir les cartes disponibles. Certaines sont même disponibles à prix réduit !", + "already_have_account": "Vous avez déjà un compte ?", + "create_account": "Créer un compte", + "privacy_policy": "Politique de confidentialité", + "welcome_to_cakepay": "Bienvenue sur Cake Pay!", + "sign_up": "S'inscrire", + "forgot_password": "Mot de passe oublié", + "reset_password": "Réinitialiser le mot de passe", + "manage_cards": "Cartes cadeaux", + "setup_your_debit_card": "Configurer votre carte de débit", + "no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où", + "how_to_use_card": "Comment utiliser cette carte", + "purchase_gift_card": "Acheter une carte-cadeau", + "verification": "Vérification", + "fill_code": "Veuillez remplir le code de vérification fourni sur votre e-mail", + "dont_get_code": "Vous ne recevez pas le code ?", + "resend_code": "Veuillez le renvoyer", + "debit_card": "Carte de débit", + "cakepay_prepaid_card": "Carte de débit prépayée CakePay", + "no_id_needed": "Aucune pièce d'identité nécessaire !", + "frequently_asked_questions": "Foire aux questions", + "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille numérique sont soumis aux conditions générales de l'accord du titulaire de carte applicable avec l'émetteur de la carte de paiement, en vigueur à partir de de temps en temps.", + "please_reference_document": "Veuillez vous référer aux documents ci-dessous pour plus d'informations.", + "cardholder_agreement": "Contrat de titulaire de carte", + "e_sign_consent": "Consentement de signature électronique", + "agree_and_continue": "Accepter et continuer", + "email_address": "Adresse e-mail", + "agree_to": "En créant un compte, vous acceptez les ", + "and": "et", + "enter_code": "Entrez le code", + "congratulations": "Félicitations !", + "you_now_have_debit_card": "Vous avez maintenant une carte de débit", + "min_amount" : "Min : ${value}", + "max_amount" : "Max : ${value}", + "enter_amount": "Entrez le montant", + "billing_address_info": "Si une adresse de facturation vous est demandée, indiquez votre adresse de livraison", + "order_physical_card": "Commander une carte physique", + "add_value": "Ajouter une valeur", + "activate": "Activer", + "get_a": "Obtenir un ", + "digital_and_physical_card": "carte de débit prépayée numérique et physique", + "get_card_note": " que vous pouvez recharger avec des devises numériques. Aucune information supplémentaire n'est nécessaire !", + "signup_for_card_accept_terms": "Inscrivez-vous pour la carte et acceptez les conditions.", + "add_fund_to_card": "Ajouter des fonds prépayés aux cartes (jusqu'à ${value})", + "use_card_info_two": "Les fonds sont convertis en USD lorsqu'ils sont détenus sur le compte prépayé, et non en devises numériques.", + "use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.", + "optionally_order_card": "Commander éventuellement une carte physique.", + "hide_details" : "Masquer les détails", + "show_details" : "Afficher les détails", + "upto": "jusqu'à ${value}", + "discount": "Économisez ${value}%", + "gift_card_amount": "Montant de la carte-cadeau", + "bill_amount": "Montant de la facture", + "you_pay": "Vous payez", + "tip": "Astuce :", + "custom": "personnalisé", + "by_cake_pay": "par Cake Pay", + "expire": "Expire", + "mm": "MM", + "yy": "AA", + "online": "En ligne", + "offline": "Hors ligne", + "gift_card_number": "Numéro de carte cadeau", + "pin_number": "Numéro PIN", + "total_saving": "Économies totales", + "last_30_days": "30 derniers jours", + "avg_savings": "Économies moy.", + "view_all": "Voir tout", + "active_cards": "Cartes actives", + "delete_account": "Supprimer le compte", + "cards": "Cartes", + "active": "Actif", + "redeemed": "racheté", + "gift_card_balance_note": "Les cartes-cadeaux avec un solde restant apparaîtront ici", + "gift_card_redeemed_note": "Les cartes-cadeaux que vous avez utilisées apparaîtront ici", + "logout": "Déconnexion", + "add_tip": "Ajouter une astuce", + "percentageOf": "sur ${amount}", + "is_percentage": "est", + "search_category": "Catégorie de recherche", + "mark_as_redeemed": "Marquer comme échangé", + "more_options": "Plus d'options", + "awaiting_payment_confirmation": "En attente de confirmation de paiement", + "transaction_sent_notice": "Si l'écran ne continue pas après 1 minute, vérifiez un explorateur de blocs et votre e-mail.", + "agree": "d'accord", + "in_store": "En magasin", + "generating_gift_card": "Génération d'une carte-cadeau", + "payment_was_received": "Votre paiement a été reçu.", + "proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.", + "order_id": "Numéro de commande", + "gift_card_is_generated": "La carte-cadeau est générée", + "open_gift_card": "Ouvrir la carte-cadeau", + "contact_support": "Contacter l'assistance", + "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 711e4d92c..03a891169 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -534,5 +534,103 @@ "search_currency": "मुद्रा खोजें", "new_template" : "नया टेम्पलेट", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", - "wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है" + "wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है", + "market_place": "मार्केटप्लेस", + "cake_pay_title": "केक पे गिफ्ट कार्ड्स", + "cake_pay_subtitle": "उपहार कार्ड खरीदें और तुरंत रिडीम करें", + "about_cake_pay": "केक पे आपको वर्चुअल संपत्ति के साथ आसानी से उपहार कार्ड खरीदने की अनुमति देता है, जिसे संयुक्त राज्य में 150,000 से अधिक व्यापारियों पर तुरंत खर्च किया जा सकता है।", + "cake_pay_account_note": "उपलब्ध कार्ड देखने के लिए एक खाता बनाएं। कुछ छूट पर भी उपलब्ध हैं!", + "ready_have_account": "क्या आपके पास पहले से ही एक खाता है?", + "create_account": "खाता बनाएं", + "privacy_policy": "गोपनीयता नीति", + "welcome_to_cakepay": "केकपे में आपका स्वागत है!", + "sign_up": "साइन अप करें", + "forgot_password": "पासवर्ड भूल गए", + "reset_password": "पासवर्ड रीसेट करें", + "gift_cards": "उपहार कार्ड", + "setup_your_debit_card": "अपना डेबिट कार्ड सेट करें", + "no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें", + "how_to_use_card": "इस कार्ड का उपयोग कैसे करें", + "purchase_gift_card": "गिफ्ट कार्ड खरीदें", + "verification": "सत्यापन", + "fill_code": "कृपया अपने ईमेल पर प्रदान किया गया सत्यापन कोड भरें", + "dont_get_code": "कोड नहीं मिला?", + "resend_code": "कृपया इसे फिर से भेजें", + "debit_card": "डेबिट कार्ड", + "cakepay_prepaid_card": "केकपे प्रीपेड डेबिट कार्ड", + "no_id_needed": "कोई आईडी नहीं चाहिए!", + "frequently_asked_questions": "अक्सर पूछे जाने वाले प्रश्न", + "debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।", + "please_reference_document": "कृपया अधिक जानकारी के लिए नीचे दिए गए दस्तावेज़ देखें।", + "cardholder_agreement": "कार्डधारक अनुबंध", + "e_sign_consent": "ई-साइन सहमति", + "agree_and_continue": "सहमत और जारी रखें", + "email_address": "ईमेल पता", + "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", + "and": "और", + "enter_code": "कोड दर्ज करें", + "congratulations":"बधाई!", + "you_now_have_debit_card": "अब आपके पास डेबिट कार्ड है", + "min_amount" : "न्यूनतम: ${value}", + "max_amount" : "अधिकतम: ${value}", + "enter_amount": "राशि दर्ज करें", + "billing_address_info": "यदि बिलिंग पता मांगा जाए, तो अपना शिपिंग पता प्रदान करें", + "order_physical_card": "फिजिकल कार्ड ऑर्डर करें", + "add_value": "मूल्य जोड़ें", + "activate": "सक्रिय करें", + "get_a": "एक प्राप्त करें", + "digital_and_physical_card": "डिजिटल और भौतिक प्रीपेड डेबिट कार्ड", + "get_card_note": " कि आप डिजिटल मुद्राओं के साथ पुनः लोड कर सकते हैं। कोई अतिरिक्त जानकारी की आवश्यकता नहीं है!", + "signup_for_card_accept_terms": "कार्ड के लिए साइन अप करें और शर्तें स्वीकार करें।", + "add_fund_to_card": "कार्ड में प्रीपेड धनराशि जोड़ें (${value} तक)", + "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।", + "use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।", + "optionally_order_card": "वैकल्पिक रूप से एक भौतिक कार्ड ऑर्डर करें।", + "hide_details": "विवरण छुपाएं", + "show_details": "विवरण दिखाएं", + "upto": "${value} तक", + "discount": "${value}% बचाएं", + "gift_card_amount": "गिफ्ट कार्ड राशि", + "bill_amount": "बिल राशि", + "you_pay": "आप भुगतान करते हैं", + "tip": "टिप:", + "custom": "कस्टम", + "by_cake_pay": "केकपे द्वारा", + "expires": "समाप्त हो जाता है", + "mm": "एमएम", + "yy": "वाईवाई", + "online": "ऑनलाइन", + "offline": "ऑफ़लाइन", + "gift_card_number": "गिफ्ट कार्ड नंबर", + "pin_number": "पिन नंबर", + "total_saving": "कुल बचत", + "last_30_days": "पिछले 30 दिन", + "avg_savings": "औसत बचत", + "view_all": "सभी देखें", + "active_cards": "सक्रिय कार्ड", + "delete_account": "खाता हटाएं", + "cards": "कार्ड", + "active": "सक्रिय", + "redeemed": "रिडीम किया गया", + "gift_card_balance_note": "गिफ्ट कार्ड शेष राशि के साथ यहां दिखाई देंगे", + "gift_card_redeemed_note": "आपके द्वारा भुनाए गए उपहार कार्ड यहां दिखाई देंगे", + "logout": "लॉगआउट", + "add_tip": "टिप जोड़ें", + "percentageOf": "${amount} का", + "is_percentage": "है", + "search_category": "खोज श्रेणी", + "mark_as_redeemed": "रिडीम किए गए के रूप में चिह्नित करें", + "more_options": "और विकल्प", + "awaiting_payment_confirmation": "भुगतान की पुष्टि की प्रतीक्षा में", + "transaction_sent_notice": "अगर 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो ब्लॉक एक्सप्लोरर और अपना ईमेल देखें।", + "agree": "सहमत", + "in_store": "स्टोर में", + "generating_gift_card": "गिफ्ट कार्ड जनरेट कर रहा है", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", + "proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।", + "order_id": "ऑर्डर आईडी", + "gift_card_is_generated": "गिफ्ट कार्ड जनरेट हुआ", + "open_gift_card": "गिफ्ट कार्ड खोलें", + "contact_support": "सहायता से संपर्क करें", + "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 209fa38b9..c25b54eb8 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -534,5 +534,103 @@ "search_currency": "Traži valutu", "new_template" : "novi predložak", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", - "wallet_name_exists": "Novčanik s tim nazivom već postoji" + "wallet_name_exists": "Novčanik s tim nazivom već postoji", + "market_place": "Tržnica", + "cake_pay_title": "Cake Pay poklon kartice", + "cake_pay_subtitle": "Kupite darovne kartice i odmah ih iskoristite", + "about_cake_pay": "Cake Pay vam omogućuje jednostavnu kupnju darovnih kartica s virtualnim sredstvima, koja se trenutno mogu potrošiti kod više od 150 000 trgovaca u Sjedinjenim Državama.", + "cake_pay_account_note": "Napravite račun da vidite dostupne kartice. Neke su čak dostupne uz popust!", + "already_have_account": "Već imate račun?", + "create_account": "Stvori račun", + "privacy_policy": "Pravila privatnosti", + "welcome_to_cakepay": "Dobro došli u Cake Pay!", + "sign_up": "Prijavite se", + "forgot_password": "Zaboravljena lozinka", + "reset_password": "Poništi lozinku", + "gift_cards": "Ajándékkártya", + "setup_your_debit_card": "Postavite svoju debitnu karticu", + "no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", + "how_to_use_card": "Kako koristiti ovu karticu", + "purchase_gift_card": "Kupnja darovne kartice", + "verification": "Potvrda", + "fill_code": "Molimo vas da ispunite kontrolni kod koji ste dobili na svojoj e-pošti", + "dont_get_code": "Ne dobivate kod?", + "resend_code": "Molimo da ga ponovno pošaljete", + "debit_card": "Debitna kartica", + "cakepay_prepaid_card": "CakePay unaprijed plaćena debitna kartica", + "no_id_needed": "Nije potreban ID!", + "frequently_asked_questions": "Često postavljana pitanja", + "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", + "please_reference_document": "Molimo pogledajte dokumente ispod za više informacija.", + "cardholder_agreement": "Ugovor s vlasnikom kartice", + "e_sign_consent": "E-Sign pristanak", + "agree_and_continue": "Slažem se i nastavi", + "email_address": "Adresa e-pošte", + "agree_to": "Stvaranjem računa pristajete na ", + "and": "i", + "enter_code": "Unesite kod", + "congratulations": "Čestitamo!", + "you_now_have_debit_card": "Sada imate debitnu karticu", + "min_amount" : "Minimalno: ${value}", + "max_amount" : "Maksimum: ${value}", + "enter_amount": "Unesite iznos", + "billing_address_info": "Ako se od vas zatraži adresa za naplatu, navedite svoju adresu za dostavu", + "order_physical_card": "Naručite fizičku karticu", + "add_value": "Dodaj vrijednost", + "activate": "Aktiviraj", + "get_a": "Nabavite ", + "digital_and_physical_card": "digitalna i fizička unaprijed plaćena debitna kartica", + "get_card_note": " koju možete ponovno napuniti digitalnim valutama. Nisu potrebne dodatne informacije!", + "signup_for_card_accept_terms": "Prijavite se za karticu i prihvatite uvjete.", + "add_fund_to_card": "Dodajte unaprijed uplaćena sredstva na kartice (do ${value})", + "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.", + "use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.", + "optionally_order_card": "Opcionalno naručite fizičku karticu.", + "hide_details" : "Sakrij pojedinosti", + "show_details": "Prikaži pojedinosti", + "upto": "do ${value}", + "discount": "Uštedite ${value}%", + "gift_card_amount": "Iznos darovne kartice", + "bill_amount": "Iznos računa", + "you_pay": "Vi plaćate", + "tip": "Savjet:", + "custom": "prilagođeno", + "by_cake_pay": "od Cake Paya", + "expires": "Ističe", + "mm": "MM", + "yy": "GG", + "online": "Na mreži", + "offline": "izvan mreže", + "gift_card_number": "Broj darovne kartice", + "pin_number": "PIN broj", + "total_saving": "Ukupna ušteda", + "last_30_days": "Zadnjih 30 dana", + "avg_savings": "Prosj. ušteda", + "view_all": "Prikaži sve", + "active_cards": "Aktivne kartice", + "delete_account": "Izbriši račun", + "cards": "Kartice", + "active": "Aktivno", + "redeemed": "otkupljeno", + "gift_card_balance_note": "Ovdje će se pojaviti darovne kartice s preostalim saldom", + "gift_card_redeemed_note": "Poklon kartice koje ste iskoristili pojavit će se ovdje", + "logout": "Odjava", + "add_tip": "Dodaj savjet", + "percentageOf": "od ${amount}", + "is_percentage": "je", + "search_category": "Kategorija pretraživanja", + "mark_as_redeemed": "Označi kao otkupljeno", + "more_options": "Više opcija", + "awaiting_payment_confirmation": "Čeka se potvrda plaćanja", + "transaction_sent_notice": "Ako se zaslon ne nastavi nakon 1 minute, provjerite block explorer i svoju e-poštu.", + "agree": "Slažem se", + "in_store": "U trgovini", + "generating_gift_card": "Generiranje darovne kartice", + "payment_was_received": "Vaša uplata je primljena.", + "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", + "order_id": "ID narudžbe", + "gift_card_is_generated": "Poklon kartica je generirana", + "open_gift_card": "Otvori darovnu karticu", + "contact_support": "Kontaktirajte podršku", + "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4b67fdc80..8383c9ad9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -534,5 +534,103 @@ "search_currency": "Cerca valuta", "new_template" : "Nuovo modello", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", - "wallet_name_exists": "Il portafoglio con quel nome è già esistito" + "wallet_name_exists": "Il portafoglio con quel nome è già esistito", + "market_place": "Mercato", + "cake_pay_title": "Carte regalo Cake Pay", + "cake_pay_subtitle": "Acquista carte regalo e riscattale all'istante", + "about_cake_pay": "Cake Pay ti consente di acquistare facilmente buoni regalo con asset virtuali, spendibili istantaneamente presso oltre 150.000 commercianti negli Stati Uniti.", + "cake_pay_account_note": "Crea un account per vedere le carte disponibili. Alcune sono anche disponibili con uno sconto!", + "already_have_account": "Hai già un account?", + "create_account": "Crea account", + "privacy_policy": "Informativa sulla privacy", + "welcome_to_cakepay": "Benvenuto in Cake Pay!", + "sign_up": "Registrati", + "forgot_password": "Password dimenticata", + "reset_password": "Reimposta password", + "gift_cards": "Carte regalo", + "setup_your_debit_card": "Configura la tua carta di debito", + "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", + "how_to_use_card": "Come usare questa carta", + "purchase_gift_card": "Acquista carta regalo", + "verification": "Verifica", + "fill_code": "Compila il codice di verifica fornito alla tua email", + "dont_get_code": "Non ricevi il codice?", + "resend_code": "Per favore, invialo nuovamente", + "debit_card": "Carta di debito", + "cakepay_prepaid_card": "Carta di debito prepagata CakePay", + "no_id_needed": "Nessun ID necessario!", + "frequently_asked_questions": "Domande frequenti", + "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore da tempo al tempo.", + "please_reference_document": "Si prega di fare riferimento ai documenti di seguito per ulteriori informazioni.", + "cardholder_agreement": "Contratto del titolare della carta", + "e_sign_consent": "Consenso alla firma elettronica", + "agree_and_continue": "Accetta e continua", + "email_address": "Indirizzo e-mail", + "agree_to": "Creando un account accetti il ​​", + "and": "e", + "enter_code": "Inserisci codice", + "congratulation": "Congratulazioni!", + "you_now_have_debit_card": "Ora hai una carta di debito", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Inserisci importo", + "billing_address_info": "Se ti viene richiesto un indirizzo di fatturazione, fornisci il tuo indirizzo di spedizione", + "order_physical_card": "Ordine carta fisica", + "add_value": "Aggiungi valore", + "activate": "Attiva", + "get_a": "Prendi un ", + "digital_and_physical_card": "carta di debito prepagata digitale e fisica", + "get_card_note": "che puoi ricaricare con le valute digitali. Non sono necessarie informazioni aggiuntive!", + "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", + "add_fund_to_card": "Aggiungi fondi prepagati alle carte (fino a ${value})", + "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.", + "use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.", + "optional_order_card": "Ordina facoltativamente una carta fisica.", + "hide_details" : "Nascondi dettagli", + "show_details": "Mostra dettagli", + "upto": "fino a ${value}", + "discount": "Risparmia ${value}%", + "gift_card_amount": "Importo del buono regalo", + "bill_amount": "Importo della fattura", + "you_pay": "Tu paghi", + "tip": "Suggerimento:", + "custom": "personalizzato", + "by_cake_pay": "da Cake Pay", + "expires": "Scade", + "mm": "mm", + "yy": "YY", + "online": "in linea", + "offline": "Offline", + "gift_card_number": "Numero del buono regalo", + "pin_number": "Numero PIN", + "total_saving": "Risparmio totale", + "last_30_days": "Ultimi 30 giorni", + "avg_savings": "Risparmio medio", + "view_all": "Visualizza tutto", + "active_cards": "Carte attive", + "delete_account": "Elimina account", + "cards": "Carte", + "active": "Attivo", + "redeemed": "Redento", + "gift_card_balance_note": "Le carte regalo con un saldo residuo appariranno qui", + "gift_card_redeemed_note": "Le carte regalo che hai riscattato appariranno qui", + "logout": "Logout", + "add_tip": "Aggiungi suggerimento", + "percentageOf": "di ${amount}", + "is_percentage": "è", + "search_category": "Categoria di ricerca", + "mark_as_redeemed": "Segna come riscattato", + "more_options": "Altre opzioni", + "waiting_payment_confirmation": "In attesa di conferma del pagamento", + "transaction_sent_notice": "Se lo schermo non procede dopo 1 minuto, controlla un block explorer e la tua email.", + "agree": "d'accordo", + "in_store": "In negozio", + "generating_gift_card": "Generazione carta regalo", + "payment_was_received": "Il tuo pagamento è stato ricevuto.", + "proceed_after_one_minute": "Se lo schermo non procede dopo 1 minuto, controlla la tua email.", + "order_id": "ID ordine", + "gift_card_is_generated": "Il buono regalo è stato generato", + "open_gift_card": "Apri carta regalo", + "contact_support": "Contatta l'assistenza", + "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9a3f739e5..0ee7b5f29 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -534,5 +534,103 @@ "search_currency": "検索通貨", "new_template" : "新しいテンプレート", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", - "wallet_name_exists": "その名前のウォレットはすでに存在しています" + "wallet_name_exists": "その名前のウォレットはすでに存在しています", + "market_place": "Marketplace", + "cake_pay_title": "ケーキペイギフトカード", + "cake_pay_subtitle": "ギフトカードを購入してすぐに利用できます", + "about_cake_pay": "Cake Payを使用すると、仮想資産を含むギフトカードを簡単に購入でき、米国内の150,000を超える加盟店ですぐに利用できます。", + "cake_pay_account_note": "アカウントを作成して、利用可能なカードを確認してください。割引価格で利用できるカードもあります!", + "already_have_account": "すでにアカウントをお持ちですか?", + "create_account": "アカウントの作成", + "privacy_policy": "プライバシーポリシー", + "welcome_to_cakepay": "Cake Payへようこそ!", + "sign_up": "サインアップ", + "forgot_password": "パスワードを忘れた", + "reset_password": "パスワードのリセット", + "gift_cards": "ギフトカード", + "setup_your_debit_card": "デビットカードを設定してください", + "no_id_required": "IDは必要ありません。どこにでも補充して使用できます", + "how_to_use_card": "このカードの使用方法", + "purchase_gift_card": "ギフトカードを購入", + "verification" : "検証", + "fill_code": "メールアドレスに記載されている確認コードを入力してください", + "dont_get_code": "コードを取得しませんか?", + "resend_code": "再送してください", + "debit_card": "デビットカード", + "cakepay_prepaid_card": "CakePayプリペイドデビットカード", + "no_id_needed": "IDは必要ありません!", + "frequently_asked_questions": "よくある質問", + "debit_card_terms": "このデジタルウォレットでの支払いカード番号(および支払いカード番号に対応する資格情報)の保存と使用には、支払いカード発行者との該当するカード所有者契約の利用規約が適用されます。時々。", + "please_reference_document": "詳細については、以下のドキュメントを参照してください。", + "cardholder_agreement": "カード所有者契約", + "e_sign_consent": "電子署名の同意", + "agree_and_continue": "同意して続行", + "email_address": "メールアドレス", + "agree_to": "アカウントを作成することにより、", + "and": "と", + "enter_code": "コードを入力", + "congratulations": "おめでとうございます!", + "you_now_have_debit_card": "デビットカードができました", + "min_amount": "最小: ${value}", + "max_amount": "最大: ${value}", + "enter_amount": "金額を入力", + "billing_address_info": "請求先住所を尋ねられた場合は、配送先住所を入力してください", + "order_physical_card": "物理カードの注文", + "add_value": "付加価値", + "activate": "アクティブ化", + "get_a": "Get a", + "digital_and_physical_card": "デジタルおよび物理プリペイドデビットカード", + "get_card_note": "デジタル通貨でリロードできます。追加情報は必要ありません!", + "signup_for_card_accept_terms": "カードにサインアップして、利用規約に同意してください。", + "add_fund_to_card": "プリペイド資金をカードに追加します(最大 ${value})", + "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。", + "use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。", + "optionally_order_card": "オプションで物理カードを注文します。", + "hide_details": "詳細を非表示", + "show_details": "詳細を表示", + "upto": "up up ${value}", + "discount": "${value}%を節約", + "gift_card_amount": "ギフトカードの金額", + "bill_amount": "請求額", + "you_pay": "あなたが支払う", + "tip": "ヒント: ", + "custom": "カスタム", + "by_cake_pay": "by Cake Pay", + "expires": "Expires", + "mm": "んん", + "yy": "YY", + "online": "オンライン", + "offline": "オフライン", + "gift_card_number": "ギフトカード番号", + "pin_number": "PIN番号", + "total_saving": "合計節約額", + "last_30_days": "過去30日", + "avg_savings": "平均節約額", + "view_all": "すべて表示", + "active_cards": "アクティブカード", + "delete_account": "アカウントの削除", + "cards": "カード", + "active": "アクティブ", + "redeemed": "償還", + "gift_card_balance_note": "残高が残っているギフトカードがここに表示されます", + "gift_card_redeemed_note": "利用したギフトカードがここに表示されます", + "logout": "ログアウト", + "add_tip": "ヒントを追加", + "percentageOf": "of ${amount}", + "is_percentage": "is", + "search_category": "検索カテゴリ", + "mark_as_redeemed": "償還済みとしてマーク", + "more_options": "その他のオプション", + "awaiting_payment_confirmation": "支払い確認を待っています", + "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。", + "agree": "同意する", + "in_store": "インストア", + "generated_gift_card": "ギフトカードの生成", + "payment_was_received": "お支払いを受け取りました。", + "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", + "order_id": "注文ID", + "gift_card_is_generated": "ギフトカードが生成されます", + "open_gift_card": "オープンギフトカード", + "contact_support": "サポートに連絡する", + "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index afe78fa3a..d7ee849aa 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -534,5 +534,103 @@ "search_currency": "통화 검색", "new_template" : "새 템플릿", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", - "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다." + "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다.", + "market_place": "마켓플레이스", + "cake_pay_title": "케이크 페이 기프트 카드", + "cake_pay_subtitle": "기프트 카드를 구매하고 즉시 사용", + "about_cake_pay": "Cake Pay를 사용하면 미국 내 150,000개 이상의 가맹점에서 즉시 사용할 수 있는 가상 자산이 포함된 기프트 카드를 쉽게 구입할 수 있습니다.", + "cake_pay_account_note": "사용 가능한 카드를 보려면 계정을 만드십시오. 일부는 할인된 가격으로 사용 가능합니다!", + "already_have_account": "이미 계정이 있습니까?", + "create_account": "계정 만들기", + "privacy_policy": "개인 정보 보호 정책", + "welcome_to_cakepay": "Cake Pay에 오신 것을 환영합니다!", + "sign_up": "가입", + "forgot_password": "비밀번호 찾기", + "reset_password": "비밀번호 재설정", + "gift_cards": "기프트 카드", + "setup_your_debit_card": "직불카드 설정", + "no_id_required": "신분증이 필요하지 않습니다. 충전하고 어디에서나 사용하세요", + "how_to_use_card": "이 카드를 사용하는 방법", + "purchase_gift_card": "기프트 카드 구매", + "verification": "검증", + "fill_code": "이메일에 제공된 인증 코드를 입력하세요.", + "dont_get_code": "코드를 받지 못하셨습니까?", + "resend_code": "다시 보내주세요", + "debit_card": "직불 카드", + "cakepay_prepaid_card": "CakePay 선불 직불 카드", + "no_id_needed": "ID가 필요하지 않습니다!", + "frequently_asked_questions": "자주 묻는 질문", + "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", + "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", + "cardholder_agreement": "카드 소유자 계약", + "e_sign_consent": "전자 서명 동의", + "agree_and_continue": "동의 및 계속", + "email_address": "이메일 주소", + "agree_to": "계정을 생성하면 ", + "and": "그리고", + "enter_code": "코드 입력", + "congratulations": "축하합니다!", + "you_now_have_debit_card": "이제 직불카드가 있습니다.", + "min_amount" : "최소: ${value}", + "max_amount" : "최대: ${value}", + "enter_amount": "금액 입력", + "billing_address_info": "청구서 수신 주소를 묻는 메시지가 표시되면 배송 주소를 입력하세요.", + "order_physical_card": "물리적 카드 주문", + "add_value": "값 추가", + "activate": "활성화", + "get_a": "가져오기", + "digital_and_physical_card": " 디지털 및 실제 선불 직불 카드", + "get_card_note": " 디지털 통화로 충전할 수 있습니다. 추가 정보가 필요하지 않습니다!", + "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의합니다.", + "add_fund_to_card": "카드에 선불 금액 추가(최대 ${value})", + "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", + "use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.", + "optionally_order_card": "선택적으로 실제 카드를 주문하십시오.", + "hide_details" : "세부 정보 숨기기", + "show_details" : "세부정보 표시", + "upto": "최대 ${value}", + "discount": "${value}% 절약", + "gift_card_amount": "기프트 카드 금액", + "bill_amount": "청구 금액", + "you_pay": "당신이 지불합니다", + "tip": "팁:", + "custom": "커스텀", + "by_cake_pay": "Cake Pay로", + "expires": "만료", + "mm": "mm", + "YY": "YY", + "online": "온라인", + "offline": "오프라인", + "gift_card_number": "기프트 카드 번호", + "pin_number": "PIN 번호", + "total_saving": "총 절감액", + "last_30_days": "지난 30일", + "avg_savings": "평균 절감액", + "view_all": "모두 보기", + "active_cards": "활성 카드", + "delete_account": "계정 삭제", + "cards": "카드", + "active": "활성", + "redeemed": "구함", + "gift_card_balance_note": "잔액이 남아 있는 기프트 카드가 여기에 표시됩니다.", + "gift_card_redeemed_note": "사용한 기프트 카드가 여기에 표시됩니다.", + "logout": "로그아웃", + "add_tip": "팁 추가", + "percentageOf": "${amount} 중", + "is_percentage": "이다", + "search_category": "검색 카테고리", + "mark_as_redeemed": "사용한 것으로 표시", + "more_options": "추가 옵션", + "awaiting_payment_confirmation": "결제 확인 대기 중", + "transaction_sent_notice": "1분 후에도 화면이 진행되지 않으면 블록 익스플로러와 이메일을 확인하세요.", + "agree": "동의하다", + "in_store": "매장 내", + "generating_gift_card": "기프트 카드 생성 중", + "payment_was_received": "결제가 접수되었습니다.", + "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", + "order_id": "주문 ID", + "gift_card_is_generated": "기프트 카드가 생성되었습니다", + "open_gift_card": "기프트 카드 열기", + "contact_support": "지원팀에 문의", + "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다." } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 5dfd9c3f2..967cf1e38 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -534,5 +534,103 @@ "search_currency": "Zoek valuta", "new_template" : "Nieuwe sjabloon", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", - "wallet_name_exists": "Portemonnee met die naam bestaat al" + "wallet_name_exists": "Portemonnee met die naam bestaat al", + "market_place": "Marktplaats", + "cake_pay_title": "Cake Pay-cadeaubonnen", + "cake_pay_subtitle": "Koop cadeaubonnen en wissel ze direct in", + "about_cake_pay": "Met Cake Pay kunt u eenvoudig cadeaubonnen kopen met virtuele activa, die direct kunnen worden uitgegeven bij meer dan 150.000 handelaren in de Verenigde Staten.", + "cake_pay_account_note": "Maak een account aan om de beschikbare kaarten te zien. Sommige zijn zelfs met korting verkrijgbaar!", + "already_have_account": "Heb je al een account?", + "create_account": "Account aanmaken", + "privacy_policy": "Privacybeleid", + "welcome_to_cakepay": "Welkom bij Cake Pay!", + "sign_up": "Aanmelden", + "forgot_password": "Wachtwoord vergeten", + "reset_password": "Wachtwoord resetten", + "gift_cards": "Cadeaubonnen", + "setup_your_debit_card": "Stel uw debetkaart in", + "no_id_required": "Geen ID vereist. Opwaarderen en overal uitgeven", + "how_to_use_card": "Hoe deze kaart te gebruiken", + "purchase_gift_card": "Cadeaubon kopen", + "verification": "Verificatie", + "fill_code": "Vul de verificatiecode in die u in uw e-mail hebt ontvangen", + "dont_get_code": "Geen code?", + "resend_code": "Stuur het alstublieft opnieuw", + "debit_card": "Debetkaart", + "cakepay_prepaid_card": "CakePay Prepaid Debetkaart", + "no_id_needed": "Geen ID nodig!", + "frequently_asked_questions": "Veelgestelde vragen", + "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", + "please_reference_document": "Raadpleeg de onderstaande documenten voor meer informatie.", + "cardholder_agreement": "Kaarthouderovereenkomst", + "e_sign_consent": "Toestemming e-ondertekenen", + "agree_and_continue": "Akkoord & doorgaan", + "email_address": "E-mailadres", + "agree_to": "Door een account aan te maken gaat u akkoord met de ", + "and": "en", + "enter_code": "Voer code in", + "congratulations": "gefeliciteerd!", + "you_now_have_debit_card": "Je hebt nu een debetkaart", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Voer Bedrag in", + "billing_address_info": "Als u om een ​​factuuradres wordt gevraagd, geef dan uw verzendadres op", + "order_physical_card": "Fysieke kaart bestellen", + "add_value": "Waarde toevoegen", + "activate": "Activeren", + "get_a": "Krijg een ", + "digital_and_physical_card": "digitale und physische Prepaid-Debitkarte", + "get_card_note": " die Sie mit digitaler Währung aufladen können. Keine zusätzlichen Informationen erforderlich!", + "signup_for_card_accept_terms": "Melden Sie sich für die Karte an und akzeptieren Sie die Bedingungen.", + "add_fund_to_card": "Prepaid-Guthaben zu den Karten hinzufügen (bis zu ${value})", + "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.", + "use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.", + "optional_order_card": "Optional eine physische Karte bestellen.", + "hide_details": "Details ausblenden", + "show_details": "Details anzeigen", + "upto": "bis zu ${value}", + "discount": "${value} % sparen", + "gift_card_amount": "Gutscheinbetrag", + "bill_amount": "Rechnungsbetrag", + "you_pay": "Sie bezahlen", + "tip": "Hinweis:", + "custom": "benutzerdefiniert", + "by_cake_pay": "von Cake Pay", + "expires": "Läuft ab", + "mm": "MM", + "yy": "YY", + "online": "online", + "offline": "offline", + "gift_card_number": "Geschenkkartennummer", + "pin_number": "PIN-Nummer", + "total_saving": "Gesamteinsparungen", + "last_30_days": "Letzte 30 Tage", + "avg_savings": "Durchschn. Einsparungen", + "view_all": "Alle anzeigen", + "active_cards": "Aktive Karten", + "delete_account": "Konto löschen", + "cards": "Karten", + "active": "Aktiv", + "redeemed": "Versilbert", + "gift_card_balance_note": "Geschenkkarten mit Restguthaben erscheinen hier", + "gift_card_redeemed_note": "Gutscheine, die Sie eingelöst haben, werden hier angezeigt", + "abmelden": "Abmelden", + "add_tip": "Tipp hinzufügen", + "percentageOf": "von ${amount}", + "is_percentage": "ist", + "search_category": "Suchkategorie", + "mark_as_redeemed": "Als eingelöst markieren", + "more_options": "Weitere Optionen", + "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", + "transaction_sent_notice": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie einen Block-Explorer und Ihre E-Mail.", + "agree": "stimme zu", + "in_store": "Im Geschäft", + "generating_gift_card": "Geschenkkarte wird erstellt", + "payment_was_received": "Ihre Zahlung ist eingegangen.", + "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", + "order_id": "Order-ID", + "gift_card_is_generated": "Cadeaukaart is gegenereerd", + "open_gift_card": "Geschenkkaart openen", + "contact_support": "Contact opnemen met ondersteuning", + "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 0fa9ac3a3..be58b6a17 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -534,5 +534,103 @@ "search_currency": "Wyszukaj walutę", "new_template" : "Nowy szablon", "electrum_address_disclaimer": "Za każdym razem, gdy korzystasz z jednego z nich, generujemy nowe adresy, ale poprzednie adresy nadal działają", - "wallet_name_exists": "Portfel o tej nazwie już istnieje" + "wallet_name_exists": "Portfel o tej nazwie już istnieje", + "market_place": "Rynek", + "cake_pay_title": "Karty podarunkowe Cake Pay", + "cake_pay_subtitle": "Kup karty podarunkowe i wykorzystaj je natychmiast", + "about_cake_pay": "Cake Pay umożliwia łatwe kupowanie kart podarunkowych z wirtualnymi aktywami, które można natychmiast wydać u ponad 150 000 sprzedawców w Stanach Zjednoczonych.", + "cake_pay_account_note": "Załóż konto, aby zobaczyć dostępne karty. Niektóre są nawet dostępne ze zniżką!", + "already_have_account": "Masz już konto?", + "create_account": "Utwórz konto", + "privacy_policy": "Polityka prywatności", + "welcome_to_cakepay": "Witamy w Cake Pay!", + "sign_up": "Zarejestruj się", + "forgot_password": "Zapomniałem hasła", + "reset_password": "Zresetuj hasło", + "gift_cards": "Karty podarunkowe", + "setup_your_debit_card": "Skonfiguruj swoją kartę debetową", + "no_id_required": "Nie wymagamy ID. Doładuj i wydawaj gdziekolwiek", + "how_to_use_card": "Jak korzystać z tej karty", + "purchase_gift_card": "Kup kartę podarunkową", + "verification": "Weryfikacja", + "fill_code": "Proszę wpisać kod weryfikacyjny podany w wiadomości e-mail", + "dont_get_code": "Nie odbierasz kodu?", + "resend_code": "Wyślij go ponownie", + "debit_card": "Karta debetowa", + "cakepay_prepaid_card": "Przedpłacona karta debetowa CakePay", + "no_id_needed": "Nie potrzeba ID!", + "frequently_asked_questions": "Często zadawane pytania", + "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", + "please_reference_document": "Proszę odwołać się do poniższych dokumentów, aby uzyskać więcej informacji.", + "cardholder_agreement": "Umowa posiadacza karty", + "e_sign_consent": "Zgoda na podpis elektroniczny", + "agree_and_continue": "Zgadzam się i kontynuuj", + "email_address": "Adres e-mail", + "agree_to": "Tworząc konto wyrażasz zgodę na ", + "and": "i", + "enter_code": "Wprowadź kod", + "congratulations": "gratulacje!", + "you_now_have_debit_card": "Masz teraz kartę debetową", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Wprowadź kwotę", + "billing_address_info": "Jeśli zostaniesz poproszony o podanie adresu rozliczeniowego, podaj swój adres wysyłki", + "order_physical_card": "Zamów kartę fizyczną", + "add_value": "Dodaj wartość", + "activate": "Aktywuj", + "get_a": "Zdobądź ", + "digital_and_physical_card": " cyfrowa i fizyczna przedpłacona karta debetowa", + "get_card_note": " które możesz doładować walutami cyfrowymi. Nie są potrzebne żadne dodatkowe informacje!", + "signup_for_card_accept_terms": "Zarejestruj się, aby otrzymać kartę i zaakceptuj warunki.", + "add_fund_to_card": "Dodaj przedpłacone środki do kart (do ${value})", + "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.", + "use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.", + "optionally_order_card": "Opcjonalnie zamów kartę fizyczną.", + "hide_details" : "Ukryj szczegóły", + "show_details" : "Pokaż szczegóły", + "upto": "do ${value}", + "discount": "Zaoszczędź ${value}%", + "gift_card_amount": "Kwota karty podarunkowej", + "bill_amount": "Kwota rachunku", + "you_pay": "Płacisz", + "tip": "wskazówka:", + "custom": "niestandardowy", + "by_cake_pay": "przez Cake Pay", + "expires": "Wygasa", + "mm": "MM", + "yy": "RR", + "online": "online", + "offline": "Offline", + "gift_card_number": "Numer karty podarunkowej", + "pin_number": "Numer PIN", + "total_saving": "Całkowite oszczędności", + "last_30_days": "Ostatnie 30 dni", + "avg_savings": "Śr. oszczędności", + "view_all": "Wyświetl wszystko", + "active_cards": "Aktywne karty", + "delete_account": "Usuń konto", + "cards": "Karty", + "active": "Aktywny", + "redeemed": "wykupione", + "gift_card_balance_note": "Tutaj pojawią się karty podarunkowe z pozostałym saldem", + "gift_card_redeemed_note": "Karty podarunkowe, które wykorzystałeś, pojawią się tutaj", + "logout": "Wyloguj", + "add_tip": "Dodaj wskazówkę", + "percentageOf": "z ${amount}", + "is_percentage": "jest", + "search_category": "Kategoria wyszukiwania", + "mark_as_redeemed": "Oznacz jako wykorzystany", + "more_options": "Więcej opcji", + "awaiting_payment_confirmation": "Oczekiwanie na potwierdzenie płatności", + "transaction_sent_notice": "Jeśli ekran nie pojawi się po 1 minucie, sprawdź eksplorator bloków i swój e-mail.", + "agree": "Zgadzam się", + "in_store": "W Sklepie", + "generating_gift_card": "Generowanie karty podarunkowej", + "payment_was_received": "Twoja płatność została otrzymana.", + "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", + "order_id": "Identyfikator zamówienia", + "gift_card_is_generated": "Karta podarunkowa jest generowana", + "open_gift_card": "Otwórz kartę podarunkową", + "contact_support": "Skontaktuj się z pomocą techniczną", + "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 558884ca8..50aa25100 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -534,5 +534,103 @@ "search_currency": "Pesquisar moeda", "new_template" : "Novo modelo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", - "wallet_name_exists": "A carteira com esse nome já existe" + "wallet_name_exists": "A carteira com esse nome já existe", + "market_place": "Mercado", + "cake_pay_title": "Cartões de presente de pagamento de bolo", + "cake_pay_subtitle": "Compre vales-presente e resgate instantaneamente", + "about_cake_pay": "O Cake Pay permite que você compre facilmente cartões-presente com ativos virtuais, que podem ser gastos instantaneamente em mais de 150.000 comerciantes nos Estados Unidos.", + "cake_pay_account_note": "Faça uma conta para ver os cartões disponíveis. Alguns estão até com desconto!", + "already_have_account": "Já tem uma conta?", + "create_account": "Criar conta", + "privacy_policy": "Política de privacidade", + "welcome_to_cakepay": "Bem-vindo ao Cake Pay!", + "create_account": "Registar-se", + "forgot_password": "Esqueci a senha", + "reset_password": "Redefinir senha", + "gift_cards": "Cartões de presente", + "setup_your_debit_card": "Configure seu cartão de débito", + "no_id_required": "Não é necessário ID. Recarregue e gaste em qualquer lugar", + "how_to_use_card": "Como usar este cartão", + "purchase_gift_card": "Comprar vale-presente", + "verification": "Verificação", + "fill_code": "Por favor, preencha o código de verificação fornecido ao seu e-mail", + "dont_get_code": "Não recebeu o código?", + "resend_code": "Por favor, reenvie", + "debit_card": "Cartão de débito", + "cakepay_prepaid_card": "Cartão de débito pré-pago CakePay", + "no_id_needed": "Nenhum ID necessário!", + "frequently_asked_questions": "Perguntas frequentes", + "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", + "please_reference_document": "Por favor, consulte os documentos abaixo para mais informações.", + "cardholder_agreement": "Acordo do titular do cartão", + "e_sign_consent": "Consentimento de assinatura eletrônica", + "agree_and_continue": "Concordar e continuar", + "email_address": "Endereço de e-mail", + "agree_to": "Ao criar conta você concorda com ", + "and": "e", + "enter_code": "Digite o código", + "congratulations": "Parabéns!", + "you_now_have_debit_card": "Agora você tem um cartão de débito", + "min_amount" : "Mínimo: ${valor}", + "max_amount" : "Máx.: ${valor}", + "enter_amount": "Digite o valor", + "billing_address_info": "Se for solicitado um endereço de cobrança, forneça seu endereço de entrega", + "order_physical_card": "Pedir Cartão Físico", + "add_value": "Adicionar valor", + "activate": "Ativar", + "get_a": "Obter um ", + "digital_and_physical_card": "cartão de débito pré-pago digital e físico", + "get_card_note": " que você pode recarregar com moedas digitais. Nenhuma informação adicional é necessária!", + "signup_for_card_accept_terms": "Cadastre-se no cartão e aceite os termos.", + "add_fund_to_card": "Adicionar fundos pré-pagos aos cartões (até ${value})", + "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.", + "use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.", + "opcionalmente_order_card": "Opcionalmente, peça um cartão físico.", + "hide_details" : "Ocultar detalhes", + "show_details" : "Mostrar detalhes", + "upto": "até ${value}", + "discount": "Economize ${value}%", + "gift_card_amount": "Valor do Cartão Presente", + "bill_amount": "Valor da conta", + "you_pay": "Você paga", + "tip": "Dica:", + "custom": "personalizado", + "by_cake_pay": "por Cake Pay", + "expires": "Expira", + "mm": "MM", + "yy": "aa", + "online": "Online", + "offline": "offline", + "gift_card_number": "Número do cartão-presente", + "pin_number": "Número PIN", + "total_saving": "Economia total", + "last_30_days": "Últimos 30 dias", + "avg_savings": "Poupança média", + "view_all": "Ver todos", + "active_cards": "Cartões ativos", + "delete_account": "Excluir conta", + "cards": "Cartões", + "active": "Ativo", + "redeemed": "Resgatado", + "gift_card_balance_note": "Os cartões-presente com saldo restante aparecerão aqui", + "gift_card_redeemed_note": "Os cartões-presente que você resgatou aparecerão aqui", + "logout": "Logout", + "add_tip": "Adicionar Dica", + "percentageOf": "de ${amount}", + "is_percentage": "é", + "search_category": "Categoria de pesquisa", + "mark_as_redemed": "Marcar como resgatado", + "more_options": "Mais opções", + "waiting_payment_confirmation": "Aguardando confirmação de pagamento", + "transaction_sent_notice": "Se a tela não prosseguir após 1 minuto, verifique um explorador de blocos e seu e-mail.", + "agree": "Concordo", + "in_store": "Na loja", + "generating_gift_card": "Gerando Cartão Presente", + "payment_was_received": "Seu pagamento foi recebido.", + "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", + "order_id": "ID do pedido", + "gift_card_is_generated": "Cartão presente é gerado", + "open_gift_card": "Abrir vale-presente", + "contact_support": "Contatar Suporte", + "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1dbc473ab..ce8109512 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -534,5 +534,103 @@ "search_currency": "Валюта поиска", "new_template" : "Новый шаблон", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", - "wallet_name_exists": "Кошелек с таким именем уже существует" + "wallet_name_exists": "Кошелек с таким именем уже существует", + "market_place": "Торговая площадка", + "cake_pay_title": "Подарочные карты Cake Pay", + "cake_pay_subtitle": "Купите подарочные карты и моментально погасите их", + "about_cake_pay": "Cake Pay позволяет вам легко покупать подарочные карты с виртуальными активами, которые можно мгновенно потратить в более чем 150 000 продавцов в Соединенных Штатах.", + "cake_pay_account_note": "Создайте учетную запись, чтобы увидеть доступные карты. Некоторые даже доступны со скидкой!", + "already_have_account": "У вас уже есть аккаунт?", + "create_account": "Создать аккаунт", + "privacy_policy": "Политика конфиденциальности", + "welcome_to_cakepay": "Добро пожаловать в Cake Pay!", + "sign_up": "Зарегистрироваться", + "forgot_password": "Забыли пароль", + "reset_password": "Сбросить пароль", + "gift_cards": "Подарочные карты", + "setup_your_debit_card": "Настройте свою дебетовую карту", + "no_id_required": "Идентификатор не требуется. Пополняйте и тратьте где угодно", + "how_to_use_card": "Как использовать эту карту", + "purchase_gift_card": "Купить подарочную карту", + "verification": "Проверка", + "fill_code": "Пожалуйста, введите код подтверждения, отправленный на вашу электронную почту", + "dont_get_code": "Не получить код?", + "resend_code": "Пожалуйста, отправьте еще раз", + "debit_card": "Дебетовая карта", + "cakepay_prepaid_card": "Предоплаченная дебетовая карта CakePay", + "no_id_needed": "Идентификатор не нужен!", + "frequently_asked_questions": "Часто задаваемые вопросы", + "debit_card_terms": "Хранение и использование номера вашей платежной карты (и учетных данных, соответствующих номеру вашей платежной карты) в этом цифровом кошельке регулируются положениями и условиями применимого соглашения держателя карты с эмитентом платежной карты, действующим с время от времени.", + "please_reference_document": "Пожалуйста, обратитесь к документам ниже для получения дополнительной информации.", + "cardholder_agreement": "Соглашение с держателем карты", + "e_sign_consent": "Согласие электронной подписи", + "agree_and_continue": "Согласиться и продолжить", + "email_address": "Адрес электронной почты", + "agree_to": "Создавая аккаунт, вы соглашаетесь с ", + "and" :"и", + "enter_code": "Введите код", + "congratulations": "Поздравляем!", + "you_now_have_debit_card": "Теперь у вас есть дебетовая карта", + "min_amount": "Минимум: ${value}", + "max_amount": "Макс.: ${value}", + "enter_amount": "Введите сумму", + "billing_address_info": "Если вас попросят указать платежный адрес, укажите адрес доставки", + "order_physical_card": "Заказать физическую карту", + "add_value": "Добавить значение", + "activate": "Активировать", + "get_a": "Получить ", + "digital_and_physical_card": "цифровая и физическая предоплаченная дебетовая карта", + "get_card_note": " которую вы можете пополнить цифровой валютой. Дополнительная информация не требуется!", + "signup_for_card_accept_terms": "Подпишитесь на карту и примите условия.", + "add_fund_to_card": "Добавить предоплаченные средства на карты (до ${value})", + "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.", + "use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.", + "optionly_order_card": "При желании закажите физическую карту.", + "hide_details": "Скрыть детали", + "show_details": "Показать детали", + "upto": "до ${value}", + "discount": "Сэкономьте ${value}%", + "gift_card_amount": "Сумма подарочной карты", + "bill_amount": "Сумма счета", + "you_pay": "Вы платите", + "tip": "Совет:", + "custom": "обычай", + "by_cake_pay": "от Cake Pay", + "expires": "Истекает", + "mm": "ММ", + "yy": "ГГ", + "online": "Онлайн", + "offline": "Не в сети", + "gift_card_number": "Номер подарочной карты", + "pin_number": "ПИН-код", + "total_saving": "Общая экономия", + "last_30_days": "Последние 30 дней", + "avg_savings": "Средняя экономия", + "view_all": "Просмотреть все", + "active_cards": "Активные карты", + "delete_account": "Удалить аккаунт", + "cards": "Карты", + "active": "Активный", + "redeemed": "искуплен", + "gift_card_balance_note": "Здесь будут отображаться подарочные карты с остатком на балансе", + "gift_card_redeemed_note": "Здесь будут отображаться использованные вами подарочные карты", + "logout": "Выйти", + "add_tip": "Добавить подсказку", + "percentageOf": "из ${amount}", + "is_percentage": "есть", + "search_category": "Категория поиска", + "mark_as_redeemed": "Отметить как погашенный", + "more_options": "Дополнительные параметры", + "awaiting_payment_confirmation": "Ожидается подтверждения платежа", + "transaction_sent_notice": "Если экран не отображается через 1 минуту, проверьте обозреватель блоков и свою электронную почту.", + "agree": "согласен", + "in_store": "В магазине", + "generating_gift_card": "Создание подарочной карты", + "payment_was_received": "Ваш платеж получен.", + "proceed_after_one_minute": "Если через 1 минуту экран не отображается, проверьте свою электронную почту.", + "order_id": "Идентификатор заказа", + "gift_card_is_generated": "Подарочная карта сгенерирована", + "open_gift_card": "Открыть подарочную карту", + "contact_support": "Связаться со службой поддержки", + "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin." } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index a152e35a8..04e270d57 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -533,5 +533,103 @@ "search_currency": "Шукати валюту", "new_template" : "Новий шаблон", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", - "wallet_name_exists": "Гаманець з такою назвою вже існує" + "wallet_name_exists": "Гаманець з такою назвою вже існує", + "market_place": "Ринок", + "cake_pay_title": "Подарункові картки Cake Pay", + "cake_pay_subtitle": "Купуйте подарункові картки та використовуйте їх миттєво", + "about_cake_pay": "Cake Pay дозволяє вам легко купувати подарункові картки з віртуальними активами, які можна миттєво витратити в понад 150 000 продавців у Сполучених Штатах.", + "cake_pay_account_note": "Створіть обліковий запис, щоб побачити доступні картки. Деякі навіть доступні зі знижкою!", + "already_have_account": "Вже є обліковий запис?", + "create_account": "Створити обліковий запис", + "privacy_policy": "Політика конфіденційності", + "welcome_to_cakepay": "Ласкаво просимо до Cake Pay!", + "sign_up": "Зареєструватися", + "forgot_password": "Забули пароль", + "reset_password": "Скинути пароль", + "gift_cards": "Подарункові карти", + "setup_your_debit_card": "Налаштуйте свою дебетову картку", + "no_id_required": "Ідентифікатор не потрібен. Поповнюйте та витрачайте будь-де", + "how_to_use_card": "Як використовувати цю картку", + "purchase_gift_card": "Придбати подарункову картку", + "verification": "Перевірка", + "fill_code": "Будь ласка, введіть код підтвердження, надісланий на вашу електронну адресу", + "dont_get_code": "Не отримуєте код?", + "resend_code": "Будь ласка, надішліть його повторно", + "debit_card": "Дебетова картка", + "cakepay_prepaid_card": "Передплачена дебетова картка CakePay", + "no_id_needed": "Ідентифікатор не потрібен!", + "frequently_asked_questions": "Часті запитання", + "debit_card_terms": "Зберігання та використання номера вашої платіжної картки (та облікових даних, які відповідають номеру вашої платіжної картки) у цьому цифровому гаманці регулюються Умовами відповідної угоди власника картки з емітентом платіжної картки, що діє з час від часу.", + "please_reference_document": "Для отримання додаткової інформації зверніться до документів нижче.", + "cardholder_agreement": "Угода власника картки", + "e_sign_consent": "Згода електронного підпису", + "agree_and_continue": "Погодитися та продовжити", + "email_address": "Адреса електронної пошти", + "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", + "and": "і", + "enter_code": "Введіть код", + "congratulations": "Вітаємо!", + "you_now_have_debit_card": "Тепер у вас є дебетова картка", + "min_amount": "Мінімум: ${value}", + "max_amount": "Макс: ${value}", + "enter_amount": "Введіть суму", + "billing_address_info": "Якщо буде запропоновано платіжну адресу, вкажіть свою адресу доставки", + "order_physical_card": "Замовити фізичну картку", + "add_value": "Додати значення", + "activate": "Активувати", + "get_a": "Отримати ", + "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка", + "get_card_note": " яку можна перезавантажувати цифровими валютами. Додаткова інформація не потрібна!", + "signup_for_card_accept_terms": "Зареєструйтеся на картку та прийміть умови.", + "add_fund_to_card": "Додайте передплачені кошти на картки (до ${value})", + "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.", + "use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.", + "optionally_order_card": "Необов'язково замовте фізичну картку.", + "hide_details": "Приховати деталі", + "show_details": "Показати деталі", + "upto": "до ${value}", + "discount": "Зекономте ${value}%", + "gift_card_amount": "Сума подарункової картки", + "bill_amount": "Сума рахунку", + "you_pay": "Ви платите", + "tip": "Порада:", + "custom": "на замовлення", + "by_cake_pay": "від Cake Pay", + "expires": "Закінчується", + "mm": "MM", + "yy": "YY", + "online": "Онлайн", + "offline": "Офлайн", + "gift_card_number": "Номер подарункової картки", + "pin_number": "PIN-код", + "total_saving": "Загальна економія", + "last_30_days": "Останні 30 днів", + "avg_savings": "Середня економія", + "view_all": "Переглянути все", + "active_cards": "Активні картки", + "delete_account": "Видалити обліковий запис", + "cards": "Картки", + "active": "Активний", + "redeeded": "Викуплено", + "gift_card_balance_note": "Тут з'являться подарункові картки із залишком на балансі", + "gift_card_redeemed_note": "Подарункові картки, які ви активували, відображатимуться тут", + "logout": "Вийти", + "add_tip": "Додати підказку", + "percentageOf": "${amount}", + "is_percentage": "є", + "search_category": "Категорія пошуку", + "mark_as_redeemed": "Позначити як погашене", + "more_options": "Більше параметрів", + "awaiting_payment_confirmation": "Очікується підтвердження платежу", + "transaction_sent_notice": "Якщо екран не відображається через 1 хвилину, перевірте провідник блоків і свою електронну пошту.", + "agree": "Згоден", + "in_store": "У магазині", + "generating_gift_card": "Створення подарункової картки", + "payment_was_received": "Ваш платіж отримано.", + "proceed_after_one_minute": "Якщо екран не продовжується через 1 хвилину, перевірте свою електронну пошту.", + "order_id": "Ідентифікатор замовлення", + "gift_card_is_generated": "Подарункова картка створена", + "open_gift_card": "Відкрити подарункову картку", + "contact_support": "Звернутися до служби підтримки", + "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 9b9fe7a79..14af48137 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -532,5 +532,103 @@ "search_currency": "搜索货币", "new_template" : "新模板", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", - "wallet_name_exists": "同名的钱包已经存在" + "wallet_name_exists": "同名的钱包已经存在", + "market_place": "市场", + "cake_pay_title": "Cake Pay 礼品卡", + "cake_pay_subtitle": "购买礼品卡并立即兑换", + "about_cake_pay": "Cake Pay 让您可以轻松购买带有虚拟资产的礼品卡,可立即在美国超过 150,000 家商家消费。", + "cake_pay_account_note": "注册一个账户来查看可用的卡片。有些甚至可以打折!", + "already_have_account": "已经有账号了?", + "create_account": "创建账户", + "privacy_policy": "隐私政策", + "welcome_to_cakepay": "欢迎来到 Cake Pay!", + "sign_up": "注册", + "forgot_password": "忘记密码", + "reset_password": "重置密码", + "gift_cards": "礼品卡", + "setup_your_debit_card": "设置你的借记卡", + "no_id_required": "不需要身份证。充值并在任何地方消费", + "how_to_use_card": "如何使用这张卡", + "purchase_gift_card": "购买礼品卡", + "verification": "验证", + "fill_code": "请填写提供给您邮箱的验证码", + "dont_get_code": "没有获取代码?", + "resend_code": "请重新发送", + "debit_card": "借记卡", + "cakepay_prepaid_card": "CakePay 预付借记卡", + "no_id_needed": "不需要 ID!", + "frequently_asked_questions": "常见问题", + "debit_card_terms": "您的支付卡号(以及与您的支付卡号对应的凭证)在此数字钱包中的存储和使用受适用的持卡人与支付卡发卡机构签订的协议的条款和条件的约束,自时不时。", + "please_reference_document": "请参考以下文档以获取更多信息。", + "cardholder_agreement": "持卡人协议", + "e_sign_consent": "电子签名同意", + "agree_and_continue": "同意并继续", + "email_address": "电子邮件地址", + "agree_to": "创建账户即表示您同意 ", + "and": "和", + "enter_code": "输入代码", + "congratulations": "恭喜!", + "you_now_have_debit_card": "你现在有一张借记卡", + "min_amount" : "最小值: ${value}", + "max_amount" : "最大值: ${value}", + "enter_amount": "输入金额", + "billing_address_info": "如果要求提供帐单地址,请提供您的送货地址", + "order_physical_card": "订购实体卡", + "add_value": "增加价值", + "activate": "激活", + "get_a": "得到一个", + "digital_and_physical_card": "数字和物理预付借记卡", + "get_card_note": "你可以用数字货币重新加载。不需要额外的信息!", + "signup_for_card_accept_terms": "注册卡并接受条款。", + "add_fund_to_card": "向卡中添加预付资金(最多 ${value})", + "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。", + "use_card_info_three": "在线使用电子卡或使用非接触式支付方式。", + "optionally_order_card": "可选择订购实体卡。", + "hide_details": "隐藏细节", + "show_details": "显示详细信息", + "upto": "最高 ${value}", + "discount": "节省 ${value}%", + "gift_card_amount": "礼品卡金额", + "bill_amount": "账单金额", + "you_pay": "你付钱", + "tip": "提示:", + "custom": "自定义", + "by_cake_pay": "通过 Cake Pay", + "expires": "过期", + "mm": "毫米", + "yy": "YY", + "online": "在线", + "offline": "离线", + "gift_card_number": "礼品卡号", + "pin_number": "PIN 码", + "total_saving": "总储蓄", + "last_30_days": "过去 30 天", + "avg_savings": "平均储蓄", + "view_all": "查看全部", + "active_cards": "活动卡", + "delete_account": "删除账户", + "cards": "卡片", + "active": "活跃", + "redeemed": "赎回", + "gift_card_balance_note": "有余额的礼品卡会出现在这里", + "gift_card_redeemed_note": "您兑换的礼品卡会出现在这里", + "logout": "注销", + "add_tip": "添加提示", + "percentageOf": "${amount}", + "is_percentage": "是", + "search_category": "搜索类别", + "mark_as_redeemed": "标记为已赎回", + "more_options": "更多选项", + "awaiting_payment_confirmation": "等待付款确认", + "transaction_sent_notice": "如果屏幕在 1 分钟后没有继续,请检查区块浏览器和您的电子邮件。", + "agree": "同意", + "in_store": "店内", + "generating_gift_card": "生成礼品卡", + "payment_was_received": "您的付款已收到。", + "proceed_after_one_minute": "如果屏幕在 1 分钟后没有继续,请检查您的电子邮件。", + "order_id": "订单编号", + "gift_card_is_generated": "礼品卡生成", + "open_gift_card": "打开礼品卡", + "contact_support": "联系支持", + "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡" } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 9245c9c1f..4345c0230 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -20,8 +20,8 @@ MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.3" -CAKEWALLET_BUILD_NUMBER=105 +CAKEWALLET_VERSION="4.4.4" +CAKEWALLET_BUILD_NUMBER=108 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/android/build_monero.sh b/scripts/android/build_monero.sh index 0a9ce08ff..5634aa20a 100755 --- a/scripts/android/build_monero.sh +++ b/scripts/android/build_monero.sh @@ -1,7 +1,7 @@ #!/bin/sh . ./config.sh -MONERO_BRANCH=v0.17.3.0-android +MONERO_BRANCH=release-v0.17.3.2-android MONERO_SRC_DIR=${WORKDIR}/monero git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index de6d2adf8..99f306343 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=17 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.3" -CAKEWALLET_BUILD_NUMBER=104 +CAKEWALLET_VERSION="4.4.4" +CAKEWALLET_BUILD_NUMBER=109 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_monero.sh b/scripts/ios/build_monero.sh index 2d9d32fb0..ea29c7131 100755 --- a/scripts/ios/build_monero.sh +++ b/scripts/ios/build_monero.sh @@ -2,9 +2,9 @@ . ./config.sh -MONERO_URL="https://github.com/monero-project/monero.git" +MONERO_URL="https://github.com/cake-tech/monero.git" MONERO_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/monero" -MONERO_VERSION=tags/v0.17.3.0 +MONERO_VERSION=release-v0.17.3.2 BUILD_TYPE=release PREFIX=${EXTERNAL_IOS_DIR} DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/monero diff --git a/tool/configure.dart b/tool/configure.dart index dcbac59c5..cfa675bd6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -77,7 +77,8 @@ abstract class Bitcoin { TransactionPriority deserializeBitcoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet); - Object createBitcoinTransactionCredentials(List outputs, TransactionPriority priority); + Object createBitcoinTransactionCredentials(List outputs, {TransactionPriority priority, int feeRate}); + Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority priority, int feeRate}); List getAddresses(Object wallet); String getAddress(Object wallet); @@ -146,6 +147,7 @@ import 'package:cw_monero/mnemonics/spanish.dart'; import 'package:cw_monero/mnemonics/portuguese.dart'; import 'package:cw_monero/mnemonics/french.dart'; import 'package:cw_monero/mnemonics/italian.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; """; const moneroCwPart = "part 'cw_monero.dart';"; const moneroContent = """ @@ -229,6 +231,7 @@ abstract class Monero { WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}); Map getKeys(Object wallet); Object createMoneroTransactionCreationCredentials({List outputs, TransactionPriority priority}); + Object createMoneroTransactionCreationCredentialsRaw({List outputs, TransactionPriority priority}); String formatterMoneroAmountToString({int amount}); double formatterMoneroAmountToDouble({int amount}); int formatterMoneroParseAmount({String amount}); @@ -237,6 +240,7 @@ abstract class Monero { void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); WalletService createMoneroWalletService(Box walletInfoSource); + Map pendingTransactionInfo(Object transaction); } abstract class MoneroSubaddressList { From b777dd9e7b3bfe4a2c1249e40a33021d165c7b5f Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:03:53 +0100 Subject: [PATCH 08/87] Update avaibility status for ionia merch. (#443) --- lib/ionia/ionia_merchant.dart | 19 +++++++++++++++++++ .../ionia/cards/ionia_buy_gift_card.dart | 2 +- .../ionia/cards/ionia_manage_cards_page.dart | 16 +--------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/ionia/ionia_merchant.dart b/lib/ionia/ionia_merchant.dart index 16df62da7..1e3b764cc 100644 --- a/lib/ionia/ionia_merchant.dart +++ b/lib/ionia/ionia_merchant.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart'; +import 'package:cake_wallet/generated/i18n.dart'; class IoniaMerchant { IoniaMerchant({ @@ -172,5 +173,23 @@ class IoniaMerchant { final double savingsPercentage; double get discount => savingsPercentage; + + String get avaibilityStatus { + var status = ''; + + if (isOnline) { + status += S.current.online; + } + + if (isPhysical) { + if (status.isNotEmpty) { + status = '$status & '; + } + + status = '${status}${S.current.in_store}'; + } + + return status; + } } diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart index 899b1387d..a7b3fa4a2 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -149,7 +149,7 @@ class IoniaBuyGiftCardPage extends BasePage { discount: merchant.discount, titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, subtitleColor: Theme.of(context).hintColor, - subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline, + subTitle: merchant.avaibilityStatus, logoUrl: merchant.logoUrl, ), ) diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart index 656b8676b..ba2e793eb 100644 --- a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -216,27 +216,13 @@ class _IoniaManageCardsPageBodyState extends State { separatorBuilder: (_, __) => SizedBox(height: 4), itemBuilder: (_, index) { final merchant = merchantsList[index]; - var subTitle = ''; - - if (merchant.isOnline) { - subTitle += S.of(context).online; - } - - if (merchant.isPhysical) { - if (subTitle.isNotEmpty) { - subTitle = '$subTitle & '; - } - - subTitle = '${subTitle}${S.of(context).in_store}'; - } - return CardItem( logoUrl: merchant.logoUrl, onTap: () { Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardPage, arguments: [merchant]); }, title: merchant.legalName, - subTitle: subTitle, + subTitle: merchant.avaibilityStatus, backgroundColor: Theme.of(context).textTheme.title.backgroundColor, titleColor: Theme.of(context).accentTextTheme.display3.backgroundColor, subtitleColor: Theme.of(context).accentTextTheme.display2.backgroundColor, From 66efb635ce8499fa4b56f912020d8323821fb708 Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:13:06 +0100 Subject: [PATCH 09/87] Update decimals formatting for tips group. (#444) --- lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart index 95202f472..6eeb8c93d 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -394,8 +394,8 @@ class TipButtonGroup extends StatelessWidget { child: TipButton( isSelected: _isSelected(tip.percentage), onTap: () => onSelect(tip), - caption: '${tip.percentage}%', - subTitle: '\$${tip.additionalAmount}', + caption: '${tip.percentage.toStringAsFixed(0)}%', + subTitle: '\$${tip.additionalAmount.toStringAsFixed(2)}', )); })); } From 0d2e530726a8c6709ca70573c402f3809801fec3 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 28 Jul 2022 19:14:42 +0100 Subject: [PATCH 10/87] Update build versions for Cake Wallet for ios and android. --- scripts/android/app_env.sh | 2 +- scripts/ios/app_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 4345c0230..c5de5e175 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,7 +21,7 @@ MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=108 +CAKEWALLET_BUILD_NUMBER=110 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 99f306343..038217ece 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=109 +CAKEWALLET_BUILD_NUMBER=110 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 4bbde495ee05c0be06fab203cbc99e3a83aa4cae Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:15:26 +0100 Subject: [PATCH 11/87] Update build versions for Cake Wallet for ios and android. (#445) --- scripts/android/app_env.sh | 2 +- scripts/ios/app_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 4345c0230..c5de5e175 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,7 +21,7 @@ MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=108 +CAKEWALLET_BUILD_NUMBER=110 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 99f306343..038217ece 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=109 +CAKEWALLET_BUILD_NUMBER=110 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 086b2e439e11d69c17507fd2721690ff046e52a9 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer Date: Thu, 28 Jul 2022 13:31:33 -0500 Subject: [PATCH 12/87] Small EN string updates for Ionia (#446) --- res/values/strings_en.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c66f9a6e6..89fc45577 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -591,8 +591,8 @@ "upto": "up to ${value}", "discount": "Save ${value}%", "gift_card_amount": "Gift Card Amount", - "bill_amount": "Bill amount", - "you_pay": "You pay", + "bill_amount": "Bill Amount", + "you_pay": "You Pay", "tip": "Tip:", "custom": "custom", "by_cake_pay": "by Cake Pay", @@ -605,7 +605,7 @@ "pin_number": "PIN number", "total_saving": "Total Savings", "last_30_days": "Last 30 days", - "avg_savings": "Avg. savings", + "avg_savings": "Avg. Savings", "view_all": "View all", "active_cards": "Active cards", "delete_account": "Delete Account", @@ -621,7 +621,7 @@ "search_category": "Search category", "mark_as_redeemed": "Mark As Redeemed", "more_options": "More Options", - "awaiting_payment_confirmation": "Awaiting payment confirmation", + "awaiting_payment_confirmation": "Awaiting Payment Confirmation", "transaction_sent_notice": "If the screen doesn’t proceed after 1 minute, check a block explorer and your email.", "agree": "Agree", "in_store": "In Store", @@ -632,5 +632,5 @@ "gift_card_is_generated": "Gift Card is generated", "open_gift_card": "Open Gift Card", "contact_support": "Contact Support", - "gift_cards_unavailable": "Gift cards are available to purchase only through Monero, Bitcoin, and Litecoin at this time" + "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time" } From 2bf95d63e44fa46f935d9245e8d3b907d9e00332 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer Date: Thu, 28 Jul 2022 13:32:24 -0500 Subject: [PATCH 13/87] Fix privacy policy spelling error (#429) No material changes --- PRIVACY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 30e6a7552..d740dcba8 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ Privacy Policy -Last modified: April 29, 2022 +Last modified: July 21, 2022 Introduction ============ @@ -42,7 +42,7 @@ Information We May Receive But Do Not Retain We receive but do NOT store information from and about users of our App, including: - The device IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to supported cryptocurrency networks. - We receive this infoirmation: + We receive this information: - Automatically as you use the App. This data is provided by connecting to the Nodes and price API maintained by Cake Labs. You have the right to choose not to provide synchronization data to Cake Labs by choosing a different Node. We provide a list of Nodes in the app that include our own and third party Nodes, or you can use your own Node (which we recommend). From 0d519d180e8044a06ec17c3638e500c07bd27dda Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer Date: Thu, 28 Jul 2022 13:33:33 -0500 Subject: [PATCH 14/87] Add FIO Crypto Handles and fiat buy/sell to readme (#417) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b68d624d4..bd74ccfbc 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ * Select your own custom nodes/servers * Address book * Backup to external location or iCloud -* Send to OpenAlias, Unstoppable Domains, and Yats +* Send to OpenAlias, Unstoppable Domains, Yats, and FIO Crypto Handles * Set custom fee levels * Store local transaction notes * Extremely simple user experience @@ -40,12 +40,15 @@ * Bitcoin coin control (specify specific outputs to spend) * Automatically generate new addresses * Specify multiple recipients for batch sending +* Buy BTC with over a dozen fiat currencies +* Sell BTC for USD ### Litecoin Specific Features * Litecoin coin control (specify specific outputs to spend) * Automatically generate new addresses * Specify multiple recipients for batch sending +* Buy LTC with over a dozen fiat currencies ### Haven Specific Features From 0264b4571c5fa4292d27e030bbae977ade539334 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer Date: Thu, 28 Jul 2022 13:48:43 -0500 Subject: [PATCH 15/87] Clearer English text (#412) Extremely low priority. Uses clearer wording for `wallet_name_exists` with specific calls to action. Co-authored-by: mkyq <53115730+mkyq@users.noreply.github.com> --- res/values/strings_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 89fc45577..9fc0b743e 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -534,7 +534,7 @@ "search_currency": "Search currency", "new_template" : "New Template", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", - "wallet_name_exists": "Wallet with that name has already existed", + "wallet_name_exists": "A wallet with that name already exists. Please choose a different name or rename the other wallet first.", "market_place": "Marketplace", "cake_pay_title": "Cake Pay Gift Cards", "cake_pay_subtitle": "Buy gift cards and redeem instantly", From e2c424d1fce896e19c8085f39be4ab610cdd2612 Mon Sep 17 00:00:00 2001 From: M Date: Fri, 29 Jul 2022 19:22:29 +0100 Subject: [PATCH 16/87] Add formatted crypto amount to xmr transaction for anypay transactions. Hide order id and payment id if they are not exists on payment status screen. --- lib/ionia/ionia_anypay.dart | 1 + .../screens/ionia/cards/ionia_payment_status_page.dart | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/ionia/ionia_anypay.dart b/lib/ionia/ionia_anypay.dart index b9b53498a..a6527515a 100644 --- a/lib/ionia/ionia_anypay.dart +++ b/lib/ionia/ionia_anypay.dart @@ -46,6 +46,7 @@ class IoniaAnyPay { isParsedAddress: false, address: out.address, cryptoAmount: moneroAmountToString(amount: out.amount), + formattedCryptoAmount: out.amount, sendAll: false)).toList(), priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY case AnyPayChain.btc: diff --git a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart index d264842e6..0c2fc13df 100644 --- a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart +++ b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart @@ -104,10 +104,12 @@ class _IoniaPaymentStatusPageBodyBodyState extends State<_IoniaPaymentStatusPage .committedInfo .transactions .map((transaction) => buildDescriptionTileWithCopy(context, S.of(context).transaction_details_transaction_id, transaction.id)), - Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id), - Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId), + if (widget.viewModel.paymentInfo.ioniaOrder.id != null) + ...[Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id)], + if (widget.viewModel.paymentInfo.ioniaOrder.paymentId != null) + ...[Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId)], ])) ]), SizedBox(height: 40), From b17e9f9815d53020bd684a76ce7b490541f67224 Mon Sep 17 00:00:00 2001 From: M Date: Fri, 29 Jul 2022 19:23:11 +0100 Subject: [PATCH 17/87] Change build number for Cake Wallet ios and android. --- scripts/android/app_env.sh | 2 +- scripts/ios/app_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index c5de5e175..12388b19d 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,7 +21,7 @@ MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=110 +CAKEWALLET_BUILD_NUMBER=111 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 038217ece..ae9e41779 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=110 +CAKEWALLET_BUILD_NUMBER=111 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 3cfe2ec5322a3dd199b533dc4b28eb34320ce5d4 Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:24:40 +0100 Subject: [PATCH 18/87] Fix/xmr anypay tx fix hide order (#447) * Update build versions for Cake Wallet for ios and android. * Add formatted crypto amount to xmr transaction for anypay transactions. Hide order id and payment id if they are not exists on payment status screen. * Change build number for Cake Wallet ios and android. --- lib/ionia/ionia_anypay.dart | 1 + .../screens/ionia/cards/ionia_payment_status_page.dart | 10 ++++++---- scripts/android/app_env.sh | 2 +- scripts/ios/app_env.sh | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/ionia/ionia_anypay.dart b/lib/ionia/ionia_anypay.dart index b9b53498a..a6527515a 100644 --- a/lib/ionia/ionia_anypay.dart +++ b/lib/ionia/ionia_anypay.dart @@ -46,6 +46,7 @@ class IoniaAnyPay { isParsedAddress: false, address: out.address, cryptoAmount: moneroAmountToString(amount: out.amount), + formattedCryptoAmount: out.amount, sendAll: false)).toList(), priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY case AnyPayChain.btc: diff --git a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart index d264842e6..0c2fc13df 100644 --- a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart +++ b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart @@ -104,10 +104,12 @@ class _IoniaPaymentStatusPageBodyBodyState extends State<_IoniaPaymentStatusPage .committedInfo .transactions .map((transaction) => buildDescriptionTileWithCopy(context, S.of(context).transaction_details_transaction_id, transaction.id)), - Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id), - Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId), + if (widget.viewModel.paymentInfo.ioniaOrder.id != null) + ...[Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id)], + if (widget.viewModel.paymentInfo.ioniaOrder.paymentId != null) + ...[Divider(height: 30), + buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId)], ])) ]), SizedBox(height: 40), diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index c5de5e175..12388b19d 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,7 +21,7 @@ MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=110 +CAKEWALLET_BUILD_NUMBER=111 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 038217ece..ae9e41779 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=110 +CAKEWALLET_BUILD_NUMBER=111 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From ec75480d16f69e3c113628fbd9a0ce63c7366948 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 2 Aug 2022 11:25:55 +0100 Subject: [PATCH 19/87] Change ionia api to production endpoint. --- lib/ionia/ionia_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart index 6784e2ba1..ca3eae03d 100644 --- a/lib/ionia/ionia_api.dart +++ b/lib/ionia/ionia_api.dart @@ -9,7 +9,7 @@ import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; class IoniaApi { - static const baseUri = 'apistaging.ionia.io'; + static const baseUri = 'api.ionia.io'; static const pathPrefix = 'cake'; static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser'); static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail'); From 7b7525750d25d87300a3cc596f3eb3243b1cb521 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 2 Aug 2022 11:26:24 +0100 Subject: [PATCH 20/87] Update version for monero.com app. --- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 12388b19d..1ff9ebc1e 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,8 +14,8 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.7" -MONERO_COM_BUILD_NUMBER=14 +MONERO_COM_VERSION="1.0.8" +MONERO_COM_BUILD_NUMBER=15 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index ae9e41779..8e5bcd0e6 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,8 +13,8 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.7" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_VERSION="1.0.8" +MONERO_COM_BUILD_NUMBER=18 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" From a0bec37fc89d771a940f98db3f1fc58a5a26ecf5 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 2 Aug 2022 11:27:58 +0100 Subject: [PATCH 21/87] Update build number to cake wallet. --- scripts/android/app_env.sh | 2 +- scripts/ios/app_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 1ff9ebc1e..3c0cb2abd 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,7 +21,7 @@ MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=111 +CAKEWALLET_BUILD_NUMBER=112 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 8e5bcd0e6..1c78d8044 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=111 +CAKEWALLET_BUILD_NUMBER=112 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 4c80e92ecc8f49f8efb0dcc570526537b025e552 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 2 Aug 2022 15:11:46 +0100 Subject: [PATCH 22/87] Fix for formatting amount for any_pay_payment totalAmount. --- lib/anypay/any_pay_payment.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/anypay/any_pay_payment.dart b/lib/anypay/any_pay_payment.dart index 295ee2dc4..a2aac1d07 100644 --- a/lib/anypay/any_pay_payment.dart +++ b/lib/anypay/any_pay_payment.dart @@ -1,8 +1,8 @@ import 'package:cake_wallet/anypay/any_pay_chain.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_core/monero_amount_format.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/monero/monero.dart'; class AnyPayPayment { AnyPayPayment({ @@ -45,11 +45,11 @@ class AnyPayPayment { .fold(0, (int outAcc, out) => outAcc + out.amount)); switch (chain) { case AnyPayChain.xmr: - return moneroAmountToString(amount: total); + return monero.formatterMoneroAmountToString(amount: total); case AnyPayChain.btc: - return bitcoinAmountToString(amount: total); + return bitcoin.formatterBitcoinAmountToString(amount: total); case AnyPayChain.ltc: - return bitcoinAmountToString(amount: total); + return bitcoin.formatterBitcoinAmountToString(amount: total); default: return null; } From c3808c416d333da131c909e5bf0313d513a04780 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 4 Aug 2022 14:24:28 +0100 Subject: [PATCH 23/87] Update scripts for build monero lib for ios. --- cw_monero/ios/cw_monero.podspec | 7 +++++++ ios/Podfile.lock | 6 +++++- scripts/ios/build_monero.sh | 23 ++++++++++------------- scripts/ios/build_monero_all.sh | 1 + scripts/ios/build_openssl.sh | 2 +- scripts/ios/build_unbound.sh | 27 +++++++++++++++++++++++++++ scripts/ios/setup.sh | 1 + 7 files changed, 52 insertions(+), 15 deletions(-) create mode 100755 scripts/ios/build_unbound.sh diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec index 8262427d8..d99bba923 100644 --- a/cw_monero/ios/cw_monero.podspec +++ b/cw_monero/ios/cw_monero.podspec @@ -34,6 +34,13 @@ Pod::Spec.new do |s| sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } end + s.subspec 'Unbound' do |unbound| + unbound.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' + unbound.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libunbound.a' + unbound.libraries = 'unbound' + unbound.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + s.subspec 'Boost' do |boost| boost.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h', boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libboost.a', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d527812b2..af603c115 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -32,6 +32,7 @@ PODS: - cw_monero/Monero (= 0.0.2) - cw_monero/OpenSSL (= 0.0.2) - cw_monero/Sodium (= 0.0.2) + - cw_monero/Unbound (= 0.0.2) - cw_shared_external - Flutter - cw_monero/Boost (0.0.2): @@ -46,6 +47,9 @@ PODS: - cw_monero/Sodium (0.0.2): - cw_shared_external - Flutter + - cw_monero/Unbound (0.0.2): + - cw_shared_external + - Flutter - cw_shared_external (0.0.1): - cw_shared_external/Boost (= 0.0.1) - cw_shared_external/OpenSSL (= 0.0.1) @@ -214,7 +218,7 @@ SPEC CHECKSUMS: connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a - cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375 + cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 devicelocale: b22617f40038496deffba44747101255cee005b0 diff --git a/scripts/ios/build_monero.sh b/scripts/ios/build_monero.sh index ea29c7131..1e082bfb0 100755 --- a/scripts/ios/build_monero.sh +++ b/scripts/ios/build_monero.sh @@ -4,7 +4,7 @@ MONERO_URL="https://github.com/cake-tech/monero.git" MONERO_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/monero" -MONERO_VERSION=release-v0.17.3.2 +MONERO_VERSION=release-v0.18.0.0 BUILD_TYPE=release PREFIX=${EXTERNAL_IOS_DIR} DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/monero @@ -18,6 +18,9 @@ git submodule update --init --force mkdir -p build cd .. +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" if [ -z $INSTALL_PREFIX ]; then INSTALL_PREFIX=${ROOT_DIR}/monero @@ -46,20 +49,14 @@ cmake -D IOS=ON \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -DSTATIC=ON \ -DBUILD_GUI_DEPS=ON \ - -DINSTALL_VENDORED_LIBUNBOUND=ON \ + -DUNBOUND_INCLUDE_DIR=${EXTERNAL_IOS_INCLUDE_DIR} \ -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ -DUSE_DEVICE_TREZOR=OFF \ ../.. -make -j4 && make install -cp external/randomx/librandomx.a ${DEST_LIB}/ -cp src/cryptonote_basic/libcryptonote_basic.a ${DEST_LIB}/ -cp src/cryptonote_basic/libcryptonote_format_utils_basic.a ${DEST_LIB}/ +make wallet_api -j4 +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR popd -done - -#only for arm64 -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR -cp ${MONERO_DIR_PATH}/lib-armv8-a/* $DEST_LIB_DIR -cp ${MONERO_DIR_PATH}/include/wallet/api/* $DEST_INCLUDE_DIR +done \ No newline at end of file diff --git a/scripts/ios/build_monero_all.sh b/scripts/ios/build_monero_all.sh index 42483323d..2b61f6db0 100755 --- a/scripts/ios/build_monero_all.sh +++ b/scripts/ios/build_monero_all.sh @@ -6,4 +6,5 @@ ./build_boost.sh ./build_sodium.sh ./build_zmq.sh +./build_unbound.sh ./build_monero.sh \ No newline at end of file diff --git a/scripts/ios/build_openssl.sh b/scripts/ios/build_openssl.sh index f6f76c0dd..128ba3a54 100755 --- a/scripts/ios/build_openssl.sh +++ b/scripts/ios/build_openssl.sh @@ -10,7 +10,7 @@ echo "============================ OpenSSL ============================" echo "Cloning Open SSL from - $OPEN_SSL_URL" git clone $OPEN_SSL_URL $OPEN_SSL_DIR_PATH cd $OPEN_SSL_DIR_PATH -./build-libssl.sh --version=1.1.1k --archs="x86_64 arm64 armv7s armv7" --targets="ios64-cross-arm64" --deprecated +./build-libssl.sh --version=1.1.1q --archs="x86_64 arm64 armv7s armv7" --targets="ios64-cross-arm64" --deprecated mv ${OPEN_SSL_DIR_PATH}/include/* $EXTERNAL_IOS_INCLUDE_DIR mv ${OPEN_SSL_DIR_PATH}/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file diff --git a/scripts/ios/build_unbound.sh b/scripts/ios/build_unbound.sh new file mode 100755 index 000000000..679956a27 --- /dev/null +++ b/scripts/ios/build_unbound.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +. ./config.sh + +UNBOUND_VERSION=1.13.2 +UNBOUND_HASH=0a13b547f3b92a026b5ebd0423f54c991e5718037fd9f72445817f6a040e1a83 +UNBOUND_URL="https://www.nlnetlabs.nl/downloads/unbound/unbound-${UNBOUND_VERSION}.tar.gz" +UNBOUND_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/unbound-${UNBOUND_VERSION}" +UNBOUND_ARCH_PATH=${EXTERNAL_IOS_SOURCE_DIR}/unbound-${UNBOUND_VERSION}.tar.gz + +echo $UNBOUND_DIR_PATH +echo "============================ Unbound ============================" +curl $UNBOUND_URL -L -o $UNBOUND_ARCH_PATH +tar -xzf $UNBOUND_ARCH_PATH -C $EXTERNAL_IOS_SOURCE_DIR +cd $UNBOUND_DIR_PATH + +export IOS_SDK=iPhone +export IOS_CPU=arm64 +export IOS_PREFIX=$EXTERNAL_IOS_DIR +export AUTOTOOLS_HOST=aarch64-apple-ios +export AUTOTOOLS_BUILD="$(./config.guess)" +source ./contrib/ios/setenv_ios.sh +./contrib/ios/install_tools.sh +./contrib/ios/install_expat.sh +./configure --build="$AUTOTOOLS_BUILD" --host="$AUTOTOOLS_HOST" --prefix="$IOS_PREFIX" --with-ssl="$IOS_PREFIX" --disable-gost --with-libexpat="$IOS_PREFIX" +make +make install \ No newline at end of file diff --git a/scripts/ios/setup.sh b/scripts/ios/setup.sh index bddfa21ed..abe8435ae 100755 --- a/scripts/ios/setup.sh +++ b/scripts/ios/setup.sh @@ -35,5 +35,6 @@ ln ./libboost.a ${CW_MONERO_EXTERNAL_LIB}/libboost.a ln ./libcrypto.a ${CW_MONERO_EXTERNAL_LIB}/libcrypto.a ln ./libssl.a ${CW_MONERO_EXTERNAL_LIB}/libssl.a ln ./libsodium.a ${CW_MONERO_EXTERNAL_LIB}/libsodium.a +ln ./libunbound.a ${CW_MONERO_EXTERNAL_LIB}/libunbound.a cp ./libmonero.a $CW_MONERO_EXTERNAL_LIB cp ../include/monero/* $CW_MONERO_EXTERNAL_INCLUDE \ No newline at end of file From aa9ed08648e158d65501656b2e893162c737b2a7 Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:27:28 +0100 Subject: [PATCH 24/87] Release 4.4.4 (#451) * Update build versions for Cake Wallet for ios and android. * Add formatted crypto amount to xmr transaction for anypay transactions. Hide order id and payment id if they are not exists on payment status screen. * Change build number for Cake Wallet ios and android. * Change ionia api to production endpoint. * Update version for monero.com app. * Update build number to cake wallet. * Fix for formatting amount for any_pay_payment totalAmount. --- lib/anypay/any_pay_payment.dart | 10 +++++----- lib/ionia/ionia_api.dart | 2 +- scripts/android/app_env.sh | 6 +++--- scripts/ios/app_env.sh | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/anypay/any_pay_payment.dart b/lib/anypay/any_pay_payment.dart index 295ee2dc4..a2aac1d07 100644 --- a/lib/anypay/any_pay_payment.dart +++ b/lib/anypay/any_pay_payment.dart @@ -1,8 +1,8 @@ import 'package:cake_wallet/anypay/any_pay_chain.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_core/monero_amount_format.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/monero/monero.dart'; class AnyPayPayment { AnyPayPayment({ @@ -45,11 +45,11 @@ class AnyPayPayment { .fold(0, (int outAcc, out) => outAcc + out.amount)); switch (chain) { case AnyPayChain.xmr: - return moneroAmountToString(amount: total); + return monero.formatterMoneroAmountToString(amount: total); case AnyPayChain.btc: - return bitcoinAmountToString(amount: total); + return bitcoin.formatterBitcoinAmountToString(amount: total); case AnyPayChain.ltc: - return bitcoinAmountToString(amount: total); + return bitcoin.formatterBitcoinAmountToString(amount: total); default: return null; } diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart index 6784e2ba1..ca3eae03d 100644 --- a/lib/ionia/ionia_api.dart +++ b/lib/ionia/ionia_api.dart @@ -9,7 +9,7 @@ import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; class IoniaApi { - static const baseUri = 'apistaging.ionia.io'; + static const baseUri = 'api.ionia.io'; static const pathPrefix = 'cake'; static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser'); static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail'); diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 12388b19d..3c0cb2abd 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.7" -MONERO_COM_BUILD_NUMBER=14 +MONERO_COM_VERSION="1.0.8" +MONERO_COM_BUILD_NUMBER=15 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=111 +CAKEWALLET_BUILD_NUMBER=112 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index ae9e41779..1c78d8044 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.7" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_VERSION="1.0.8" +MONERO_COM_BUILD_NUMBER=18 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=111 +CAKEWALLET_BUILD_NUMBER=112 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 84cb0b704cadb16395d0fadb79f7461f40d65788 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 4 Aug 2022 22:26:39 +0100 Subject: [PATCH 25/87] Fix for build openssl for ios. --- scripts/ios/build_openssl.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/ios/build_openssl.sh b/scripts/ios/build_openssl.sh index 128ba3a54..57a59500b 100755 --- a/scripts/ios/build_openssl.sh +++ b/scripts/ios/build_openssl.sh @@ -10,7 +10,8 @@ echo "============================ OpenSSL ============================" echo "Cloning Open SSL from - $OPEN_SSL_URL" git clone $OPEN_SSL_URL $OPEN_SSL_DIR_PATH cd $OPEN_SSL_DIR_PATH -./build-libssl.sh --version=1.1.1q --archs="x86_64 arm64 armv7s armv7" --targets="ios64-cross-arm64" --deprecated +./build-libssl.sh --version=1.1.1q --targets="ios-cross-arm64" --deprecated mv ${OPEN_SSL_DIR_PATH}/include/* $EXTERNAL_IOS_INCLUDE_DIR -mv ${OPEN_SSL_DIR_PATH}/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file +mv ${OPEN_SSL_DIR_PATH}/lib/libcrypto-iOS.a ${EXTERNAL_IOS_LIB_DIR}/libcrypto.a +mv ${OPEN_SSL_DIR_PATH}/lib/libssl-iOS.a ${EXTERNAL_IOS_LIB_DIR}/libssl.a \ No newline at end of file From 7662598f78eefdcd905bfaf5a718a41a7c65ec79 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Aug 2022 11:40:16 +0100 Subject: [PATCH 26/87] Update unbound script for ios. --- scripts/ios/build_unbound.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/ios/build_unbound.sh b/scripts/ios/build_unbound.sh index 679956a27..d4e4d50b1 100755 --- a/scripts/ios/build_unbound.sh +++ b/scripts/ios/build_unbound.sh @@ -2,17 +2,16 @@ . ./config.sh -UNBOUND_VERSION=1.13.2 -UNBOUND_HASH=0a13b547f3b92a026b5ebd0423f54c991e5718037fd9f72445817f6a040e1a83 +UNBOUND_VERSION=release-1.16.2 +UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" UNBOUND_URL="https://www.nlnetlabs.nl/downloads/unbound/unbound-${UNBOUND_VERSION}.tar.gz" -UNBOUND_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/unbound-${UNBOUND_VERSION}" -UNBOUND_ARCH_PATH=${EXTERNAL_IOS_SOURCE_DIR}/unbound-${UNBOUND_VERSION}.tar.gz +UNBOUND_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/unbound-1.16.2" -echo $UNBOUND_DIR_PATH echo "============================ Unbound ============================" -curl $UNBOUND_URL -L -o $UNBOUND_ARCH_PATH -tar -xzf $UNBOUND_ARCH_PATH -C $EXTERNAL_IOS_SOURCE_DIR +rm -rf ${UNBOUND_DIR_PATH} +git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_DIR_PATH} cd $UNBOUND_DIR_PATH +test `git rev-parse HEAD` = ${UNBOUND_HASH} || exit 1 export IOS_SDK=iPhone export IOS_CPU=arm64 @@ -22,6 +21,6 @@ export AUTOTOOLS_BUILD="$(./config.guess)" source ./contrib/ios/setenv_ios.sh ./contrib/ios/install_tools.sh ./contrib/ios/install_expat.sh -./configure --build="$AUTOTOOLS_BUILD" --host="$AUTOTOOLS_HOST" --prefix="$IOS_PREFIX" --with-ssl="$IOS_PREFIX" --disable-gost --with-libexpat="$IOS_PREFIX" +./configure --build="$AUTOTOOLS_BUILD" --host="$AUTOTOOLS_HOST" --prefix="$IOS_PREFIX" --with-ssl="$IOS_PREFIX" --with-libexpat="$IOS_PREFIX" make make install \ No newline at end of file From 8919fdc4d29be71d1ad17feebbc6297291aff60c Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Aug 2022 17:31:33 +0100 Subject: [PATCH 27/87] Update OpenSSL, add unbound for android build scripts --- scripts/android/build_monero.sh | 2 +- scripts/android/build_monero_all.sh | 1 + scripts/android/build_openssl.sh | 9 ++-- scripts/android/build_unbound.sh | 65 +++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100755 scripts/android/build_unbound.sh diff --git a/scripts/android/build_monero.sh b/scripts/android/build_monero.sh index 5634aa20a..a88f589ec 100755 --- a/scripts/android/build_monero.sh +++ b/scripts/android/build_monero.sh @@ -1,7 +1,7 @@ #!/bin/sh . ./config.sh -MONERO_BRANCH=release-v0.17.3.2-android +MONERO_BRANCH=release-v0.18.0.0-android MONERO_SRC_DIR=${WORKDIR}/monero git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} diff --git a/scripts/android/build_monero_all.sh b/scripts/android/build_monero_all.sh index 917c71e23..69ec37b5f 100755 --- a/scripts/android/build_monero_all.sh +++ b/scripts/android/build_monero_all.sh @@ -4,5 +4,6 @@ ./build_boost.sh ./build_openssl.sh ./build_sodium.sh +./build_unbound.sh ./build_zmq.sh ./build_monero.sh diff --git a/scripts/android/build_openssl.sh b/scripts/android/build_openssl.sh index c5dad3d41..aa668e6bf 100755 --- a/scripts/android/build_openssl.sh +++ b/scripts/android/build_openssl.sh @@ -3,10 +3,10 @@ set -e . ./config.sh -OPENSSL_FILENAME=openssl-1.1.1k.tar.gz +OPENSSL_FILENAME=openssl-1.1.1q.tar.gz OPENSSL_FILE_PATH=$WORKDIR/$OPENSSL_FILENAME -OPENSSL_SRC_DIR=$WORKDIR/openssl-1.1.1k -OPENSSL_SHA256="892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5" +OPENSSL_SRC_DIR=$WORKDIR/openssl-1.1.1q +OPENSSL_SHA256="d7939ce614029cdff0b6c20f0e2e5703158a489a72b2507b8bd51bf8c8fd10ca" ZLIB_DIR=$WORKDIR/zlib ZLIB_TAG=v1.2.11 ZLIB_COMMIT_HASH="cacf7f1d4e3d44d871b605da3b647f07d718623f" @@ -40,10 +40,9 @@ rm -rf $OPENSSL_SRC_DIR tar -xzf $OPENSSL_FILE_PATH -C $WORKDIR cd $OPENSSL_SRC_DIR -sed -i -e "s/mandroid/target\ ${TARGET}\-linux\-android/" Configure CC=clang ANDROID_NDK=$TOOLCHAIN \ ./Configure ${X_ARCH} \ - no-asm no-shared \ + no-shared no-tests \ --with-zlib-include=${PREFIX}/include \ --with-zlib-lib=${PREFIX}/lib \ --prefix=${PREFIX} \ diff --git a/scripts/android/build_unbound.sh b/scripts/android/build_unbound.sh new file mode 100755 index 000000000..8786b0f2b --- /dev/null +++ b/scripts/android/build_unbound.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +. ./config.sh + +EXPAT_VERSION=R_2_4_8 +EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" +EXPAT_SRC_DIR=$WORKDIR/libexpat + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +PREFIX=$WORKDIR/prefix_${arch} +TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + +cd $WORKDIR +rm -rf $EXPAT_SRC_DIR +git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} +cd $EXPAT_SRC_DIR +test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 +cd $EXPAT_SRC_DIR/expat + +case $arch in + "aarch") HOST="arm-linux-androideabi";; + "i686") HOST="x86-linux-android";; + *) HOST="${arch}-linux-android";; +esac + +./buildconf.sh +CC=clang CXX=clang++ ./configure --enable-static --disable-shared --prefix=${PREFIX} --host=${HOST} +make -j$THREADS +make -j$THREADS install +done + +UNBOUND_VERSION=release-1.16.2 +UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" +UNBOUND_SRC_DIR=$WORKDIR/unbound-1.16.2 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +PREFIX=$WORKDIR/prefix_${arch} +TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 + +case $arch in + "aarch") TOOLCHAIN_BIN_PATH=${TOOLCHAIN_BASE_DIR}_${arch}/arm-linux-androideabi/bin;; + *) TOOLCHAIN_BIN_PATH=${TOOLCHAIN_BASE_DIR}_${arch}/${arch}-linux-android/bin;; +esac + +PATH="${TOOLCHAIN_BIN_PATH}:${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" +echo $PATH +cd $WORKDIR +rm -rf $UNBOUND_SRC_DIR +git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_SRC_DIR} +cd $UNBOUND_SRC_DIR +test `git rev-parse HEAD` = ${UNBOUND_HASH} || exit 1 + +case $arch in + "aarch") HOST="arm-linux-androideabi";; + "i686") HOST="x86-linux-android";; + *) HOST="${arch}-linux-android";; +esac + +CC=clang CXX=clang++ ./configure --prefix=${PREFIX} --host=${HOST} --enable-static --disable-shared --disable-flto --with-ssl=${PREFIX} --with-libexpat=${PREFIX} +make -j$THREADS +make -j$THREADS install +done From d62131e649ae28fdac9744c48708ef57eb8aee48 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Aug 2022 17:51:52 +0100 Subject: [PATCH 28/87] Remove using of api key for sideshift. --- lib/exchange/sideshift/sideshift_exchange_provider.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index b828c2f6f..c512b6322 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -25,14 +25,12 @@ class SideShiftExchangeProvider extends ExchangeProvider { .expand((i) => i) .toList()); - static const apiKey = secrets.sideShiftApiKey; static const affiliateId = secrets.sideShiftAffiliateId; static const apiBaseUrl = 'https://sideshift.ai/api'; static const rangePath = '/v1/pairs'; static const orderPath = '/v1/orders'; static const quotePath = '/v1/quotes'; static const permissionPath = '/v1/permissions'; - static const apiHeaderKey = 'x-sideshift-secret'; @override ExchangeProviderDescription get description => @@ -96,7 +94,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { final _request = request as SideShiftRequest; final quoteId = await _createQuote(_request); final url = apiBaseUrl + orderPath; - final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'}; + final headers = {'Content-Type': 'application/json'}; final body = { 'type': 'fixed', 'quoteId': quoteId, @@ -137,7 +135,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future _createQuote(SideShiftRequest request) async { final url = apiBaseUrl + quotePath; - final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'}; + final headers = {'Content-Type': 'application/json'}; final depositMethod = normalizeCryptoCurrency(request.depositMethod); final settleMethod = normalizeCryptoCurrency(request.settleMethod); final body = { From f44bf9eb00d166fb06257fbd6b479246e83c8248 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Aug 2022 17:58:51 +0100 Subject: [PATCH 29/87] Update versions for Cake Wallet and Monero.com --- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 3c0cb2abd..1ba99ef0e 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.8" -MONERO_COM_BUILD_NUMBER=15 +MONERO_COM_VERSION="1.0.9" +MONERO_COM_BUILD_NUMBER=16 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=112 +CAKEWALLET_VERSION="4.4.5" +CAKEWALLET_BUILD_NUMBER=113 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 1c78d8044..4510b7213 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.8" -MONERO_COM_BUILD_NUMBER=18 +MONERO_COM_VERSION="1.0.9" +MONERO_COM_BUILD_NUMBER=19 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.4" -CAKEWALLET_BUILD_NUMBER=112 +CAKEWALLET_VERSION="4.4.5" +CAKEWALLET_BUILD_NUMBER=113 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 805d72219e5efa91ef91ff5c1488ff0ffe29b86c Mon Sep 17 00:00:00 2001 From: M Date: Wed, 10 Aug 2022 15:40:39 +0100 Subject: [PATCH 30/87] Copy lib unbound to monero dir. --- scripts/android/copy_monero_deps.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/android/copy_monero_deps.sh b/scripts/android/copy_monero_deps.sh index 55360697d..d59e9d7f0 100755 --- a/scripts/android/copy_monero_deps.sh +++ b/scripts/android/copy_monero_deps.sh @@ -24,6 +24,7 @@ esac LIB_DIR=${CW_EXRTERNAL_DIR}/${ABI}/lib INCLUDE_DIR=${CW_EXRTERNAL_DIR}/${ABI}/include +LIBANBOUND_PATH=${PREFIX}/lib/libunbound.a mkdir -p $LIB_DIR mkdir -p $INCLUDE_DIR @@ -31,6 +32,9 @@ mkdir -p $INCLUDE_DIR cp -r ${PREFIX}/lib/* $LIB_DIR cp -r ${PREFIX}/include/* $INCLUDE_DIR +if [ -f "$LIBANBOUND_PATH" ]; then + cp $LIBANBOUND_PATH ${LIB_DIR}/monero +fi done From 08f197692131316d94694fa9b8db5299f3cc2fe3 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 11 Aug 2022 17:39:59 +0300 Subject: [PATCH 31/87] fix usd value bug (#467) --- lib/src/screens/ionia/cards/ionia_buy_gift_card.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart index a7b3fa4a2..015ded0e8 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -100,9 +100,6 @@ class IoniaBuyGiftCardPage extends BasePage { color: Colors.white, fontSize: 36, ), - suffixIcon: SizedBox( - width: _width / 6, - ), prefixIcon: Padding( padding: EdgeInsets.only( top: 5.0, From d22738da6477e6f0894bd934b9e4a2a61aa5538d Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 11 Aug 2022 17:40:38 +0300 Subject: [PATCH 32/87] add note decryption from QR code (#466) --- lib/src/screens/send/widgets/send_card.dart | 1 + lib/utils/payment_request.dart | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 69858a3ac..e3cc3a2c4 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -118,6 +118,7 @@ class SendCardState extends State final paymentRequest = PaymentRequest.fromUri(uri); addressController.text = paymentRequest.address; cryptoAmountController.text = paymentRequest.amount; + noteController.text = paymentRequest.note; }, options: [ AddressTextFieldOption.paste, diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index d4338a39a..26244059f 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,18 +1,22 @@ class PaymentRequest { - PaymentRequest(this.address, this.amount); + PaymentRequest(this.address, this.amount, this.note); factory PaymentRequest.fromUri(Uri uri) { var address = ""; var amount = ""; + var note = ""; if (uri != null) { address = uri.path; amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? ""; + note = uri.queryParameters['tx_description'] + ?? uri.queryParameters['message'] ?? ""; } - return PaymentRequest(address, amount); + return PaymentRequest(address, amount, note); } final String address; final String amount; + final String note; } \ No newline at end of file From 8a740e1c41f74bd75be041214f341b2770875058 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 11 Aug 2022 17:44:27 +0300 Subject: [PATCH 33/87] change account sign-in/sign-up text (#462) --- res/values/strings_de.arb | 2 +- res/values/strings_en.arb | 2 +- res/values/strings_es.arb | 2 +- res/values/strings_fr.arb | 2 +- res/values/strings_hi.arb | 2 +- res/values/strings_hr.arb | 2 +- res/values/strings_it.arb | 2 +- res/values/strings_ja.arb | 2 +- res/values/strings_ko.arb | 2 +- res/values/strings_nl.arb | 2 +- res/values/strings_pl.arb | 2 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 2 +- res/values/strings_uk.arb | 2 +- res/values/strings_zh.arb | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d9687bc36..839d40d2a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Cake Pay-Geschenkkarten", "cake_pay_subtitle": "Geschenkkarten kaufen und sofort einlösen", "about_cake_pay": "Mit Cake Pay können Sie ganz einfach Geschenkkarten mit virtuellen Vermögenswerten kaufen, die Sie sofort bei über 150.000 Händlern in den Vereinigten Staaten ausgeben können.", - "cake_pay_account_note": "Erstellen Sie ein Konto, um die verfügbaren Karten zu sehen. Einige sind sogar mit Rabatt erhältlich!", + "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "already_have_account": "Sie haben bereits ein Konto?", "create_account": "Konto erstellen", "privacy_policy": "Datenschutzrichtlinie", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9fc0b743e..821b1a227 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Cake Pay Gift Cards", "cake_pay_subtitle": "Buy gift cards and redeem instantly", "about_cake_pay": "Cake Pay allows you to easily buy gift cards with virtual assets, spendable instantly at over 150,000 merchants in the United States.", - "cake_pay_account_note": "Make an account to see the available cards. Some are even available at a discount!", + "cake_pay_account_note": "Sign up with just an email address to see and purchase cards. Some are even available at a discount!", "already_have_account": "Already have an account?", "create_account": "Create Account", "privacy_policy": "Privacy Policy", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 084658081..9f4b6d01f 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Tarjetas de regalo Cake Pay", "cake_pay_subtitle": "Compra tarjetas de regalo y canjéalas al instante", "about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.", - "cake_pay_account_note": "Crea una cuenta para ver las tarjetas disponibles. ¡Algunas incluso están disponibles con descuento!", + "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "already_have_account": "¿Ya tienes una cuenta?", "create_account": "Crear Cuenta", "privacy_policy": "Política de privacidad", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 9817e7bef..541f60e56 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -537,7 +537,7 @@ "cake_pay_title": "Cartes cadeaux Cake Pay", "cake_pay_subtitle": "Achetez des cartes-cadeaux et échangez-les instantanément", "about_cake_pay": "Cake Pay vous permet d'acheter facilement des cartes-cadeaux avec des actifs virtuels, utilisables instantanément chez plus de 150 000 marchands aux États-Unis.", - "cake_pay_account_note": "Créez un compte pour voir les cartes disponibles. Certaines sont même disponibles à prix réduit !", + "cake_pay_account_note": "Inscrivez-vous avec juste une adresse e-mail pour voir et acheter des cartes. Certaines sont même disponibles à prix réduit !", "already_have_account": "Vous avez déjà un compte ?", "create_account": "Créer un compte", "privacy_policy": "Politique de confidentialité", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 03a891169..1060ce44e 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -539,7 +539,7 @@ "cake_pay_title": "केक पे गिफ्ट कार्ड्स", "cake_pay_subtitle": "उपहार कार्ड खरीदें और तुरंत रिडीम करें", "about_cake_pay": "केक पे आपको वर्चुअल संपत्ति के साथ आसानी से उपहार कार्ड खरीदने की अनुमति देता है, जिसे संयुक्त राज्य में 150,000 से अधिक व्यापारियों पर तुरंत खर्च किया जा सकता है।", - "cake_pay_account_note": "उपलब्ध कार्ड देखने के लिए एक खाता बनाएं। कुछ छूट पर भी उपलब्ध हैं!", + "cake_pay_account_note": "कार्ड देखने और खरीदने के लिए केवल एक ईमेल पते के साथ साइन अप करें। कुछ छूट पर भी उपलब्ध हैं!", "ready_have_account": "क्या आपके पास पहले से ही एक खाता है?", "create_account": "खाता बनाएं", "privacy_policy": "गोपनीयता नीति", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index c25b54eb8..4154c812f 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Cake Pay poklon kartice", "cake_pay_subtitle": "Kupite darovne kartice i odmah ih iskoristite", "about_cake_pay": "Cake Pay vam omogućuje jednostavnu kupnju darovnih kartica s virtualnim sredstvima, koja se trenutno mogu potrošiti kod više od 150 000 trgovaca u Sjedinjenim Državama.", - "cake_pay_account_note": "Napravite račun da vidite dostupne kartice. Neke su čak dostupne uz popust!", + "cake_pay_account_note": "Prijavite se samo s adresom e-pošte da biste vidjeli i kupili kartice. Neke su čak dostupne uz popust!", "already_have_account": "Već imate račun?", "create_account": "Stvori račun", "privacy_policy": "Pravila privatnosti", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8383c9ad9..0308f16a9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Carte regalo Cake Pay", "cake_pay_subtitle": "Acquista carte regalo e riscattale all'istante", "about_cake_pay": "Cake Pay ti consente di acquistare facilmente buoni regalo con asset virtuali, spendibili istantaneamente presso oltre 150.000 commercianti negli Stati Uniti.", - "cake_pay_account_note": "Crea un account per vedere le carte disponibili. Alcune sono anche disponibili con uno sconto!", + "cake_pay_account_note": "Iscriviti con solo un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!", "already_have_account": "Hai già un account?", "create_account": "Crea account", "privacy_policy": "Informativa sulla privacy", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 0ee7b5f29..33a9d20cc 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -539,7 +539,7 @@ "cake_pay_title": "ケーキペイギフトカード", "cake_pay_subtitle": "ギフトカードを購入してすぐに利用できます", "about_cake_pay": "Cake Payを使用すると、仮想資産を含むギフトカードを簡単に購入でき、米国内の150,000を超える加盟店ですぐに利用できます。", - "cake_pay_account_note": "アカウントを作成して、利用可能なカードを確認してください。割引価格で利用できるカードもあります!", + "cake_pay_account_note": "メールアドレスだけでサインアップして、カードを表示して購入できます。割引価格で利用できるカードもあります!", "already_have_account": "すでにアカウントをお持ちですか?", "create_account": "アカウントの作成", "privacy_policy": "プライバシーポリシー", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index d7ee849aa..edfa14184 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -539,7 +539,7 @@ "cake_pay_title": "케이크 페이 기프트 카드", "cake_pay_subtitle": "기프트 카드를 구매하고 즉시 사용", "about_cake_pay": "Cake Pay를 사용하면 미국 내 150,000개 이상의 가맹점에서 즉시 사용할 수 있는 가상 자산이 포함된 기프트 카드를 쉽게 구입할 수 있습니다.", - "cake_pay_account_note": "사용 가능한 카드를 보려면 계정을 만드십시오. 일부는 할인된 가격으로 사용 가능합니다!", + "cake_pay_account_note": "이메일 주소로 가입하면 카드를 보고 구매할 수 있습니다. 일부는 할인된 가격으로 사용 가능합니다!", "already_have_account": "이미 계정이 있습니까?", "create_account": "계정 만들기", "privacy_policy": "개인 정보 보호 정책", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 967cf1e38..35d7fccfe 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Cake Pay-cadeaubonnen", "cake_pay_subtitle": "Koop cadeaubonnen en wissel ze direct in", "about_cake_pay": "Met Cake Pay kunt u eenvoudig cadeaubonnen kopen met virtuele activa, die direct kunnen worden uitgegeven bij meer dan 150.000 handelaren in de Verenigde Staten.", - "cake_pay_account_note": "Maak een account aan om de beschikbare kaarten te zien. Sommige zijn zelfs met korting verkrijgbaar!", + "cake_pay_account_note": "Meld u aan met alleen een e-mailadres om kaarten te bekijken en te kopen. Sommige zijn zelfs met korting verkrijgbaar!", "already_have_account": "Heb je al een account?", "create_account": "Account aanmaken", "privacy_policy": "Privacybeleid", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index be58b6a17..5abf68ff5 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Karty podarunkowe Cake Pay", "cake_pay_subtitle": "Kup karty podarunkowe i wykorzystaj je natychmiast", "about_cake_pay": "Cake Pay umożliwia łatwe kupowanie kart podarunkowych z wirtualnymi aktywami, które można natychmiast wydać u ponad 150 000 sprzedawców w Stanach Zjednoczonych.", - "cake_pay_account_note": "Załóż konto, aby zobaczyć dostępne karty. Niektóre są nawet dostępne ze zniżką!", + "cake_pay_account_note": "Zarejestruj się, używając tylko adresu e-mail, aby przeglądać i kupować karty. Niektóre są nawet dostępne ze zniżką!", "already_have_account": "Masz już konto?", "create_account": "Utwórz konto", "privacy_policy": "Polityka prywatności", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 50aa25100..3cc7d0319 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Cartões de presente de pagamento de bolo", "cake_pay_subtitle": "Compre vales-presente e resgate instantaneamente", "about_cake_pay": "O Cake Pay permite que você compre facilmente cartões-presente com ativos virtuais, que podem ser gastos instantaneamente em mais de 150.000 comerciantes nos Estados Unidos.", - "cake_pay_account_note": "Faça uma conta para ver os cartões disponíveis. Alguns estão até com desconto!", + "cake_pay_account_note": "Inscreva-se com apenas um endereço de e-mail para ver e comprar cartões. Alguns estão até com desconto!", "already_have_account": "Já tem uma conta?", "create_account": "Criar conta", "privacy_policy": "Política de privacidade", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index ce8109512..e4a3d23d1 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -539,7 +539,7 @@ "cake_pay_title": "Подарочные карты Cake Pay", "cake_pay_subtitle": "Купите подарочные карты и моментально погасите их", "about_cake_pay": "Cake Pay позволяет вам легко покупать подарочные карты с виртуальными активами, которые можно мгновенно потратить в более чем 150 000 продавцов в Соединенных Штатах.", - "cake_pay_account_note": "Создайте учетную запись, чтобы увидеть доступные карты. Некоторые даже доступны со скидкой!", + "cake_pay_account_note": "Зарегистрируйтесь, указав только адрес электронной почты, чтобы просматривать и покупать карты. Некоторые даже доступны со скидкой!", "already_have_account": "У вас уже есть аккаунт?", "create_account": "Создать аккаунт", "privacy_policy": "Политика конфиденциальности", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 04e270d57..171977721 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -538,7 +538,7 @@ "cake_pay_title": "Подарункові картки Cake Pay", "cake_pay_subtitle": "Купуйте подарункові картки та використовуйте їх миттєво", "about_cake_pay": "Cake Pay дозволяє вам легко купувати подарункові картки з віртуальними активами, які можна миттєво витратити в понад 150 000 продавців у Сполучених Штатах.", - "cake_pay_account_note": "Створіть обліковий запис, щоб побачити доступні картки. Деякі навіть доступні зі знижкою!", + "cake_pay_account_note": "Зареєструйтеся, використовуючи лише адресу електронної пошти, щоб переглядати та купувати картки. Деякі навіть доступні зі знижкою!", "already_have_account": "Вже є обліковий запис?", "create_account": "Створити обліковий запис", "privacy_policy": "Політика конфіденційності", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 14af48137..ccf93bdda 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -537,7 +537,7 @@ "cake_pay_title": "Cake Pay 礼品卡", "cake_pay_subtitle": "购买礼品卡并立即兑换", "about_cake_pay": "Cake Pay 让您可以轻松购买带有虚拟资产的礼品卡,可立即在美国超过 150,000 家商家消费。", - "cake_pay_account_note": "注册一个账户来查看可用的卡片。有些甚至可以打折!", + "cake_pay_account_note": "只需使用電子郵件地址註冊即可查看和購買卡片。有些甚至可以打折!", "already_have_account": "已经有账号了?", "create_account": "创建账户", "privacy_policy": "隐私政策", From 0754d4cd45c75348c478e80ff824fa56b5ef8bfb Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 22 Aug 2022 15:18:04 +0300 Subject: [PATCH 34/87] fix search bar color (#470) --- lib/src/screens/ionia/cards/ionia_manage_cards_page.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart index ba2e793eb..7e4f02bf8 100644 --- a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -135,7 +135,7 @@ class IoniaManageCardsPage extends BasePage { width: 32, padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), + color: Theme.of(context).textTheme.title.backgroundColor, border: Border.all( color: Colors.white.withOpacity(0.2), ), @@ -264,7 +264,7 @@ class _SearchWidget extends StatelessWidget { ); return TextField( - style: TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).accentTextTheme.display3.backgroundColor), controller: controller, decoration: InputDecoration( filled: true, @@ -272,10 +272,10 @@ class _SearchWidget extends StatelessWidget { top: 10, left: 10, ), - fillColor: Colors.white.withOpacity(0.15), + fillColor: Theme.of(context).textTheme.title.backgroundColor, hintText: S.of(context).search, hintStyle: TextStyle( - color: Colors.white.withOpacity(0.6), + color: Theme.of(context).accentTextTheme.display2.backgroundColor, ), alignLabelWithHint: true, floatingLabelBehavior: FloatingLabelBehavior.never, From f559b10c97d42f0d2af0c382779f8fe8e982fd4f Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 22 Aug 2022 15:42:36 +0300 Subject: [PATCH 35/87] fix UI validation bug (#439) --- .../screens/new_wallet/new_wallet_page.dart | 58 +++++++++---------- .../wallet_restore_from_keys_form.dart | 52 ++++++++--------- .../wallet_restore_from_seed_form.dart | 38 ++++++------ lib/src/widgets/seed_widget.dart | 6 +- 4 files changed, 78 insertions(+), 76 deletions(-) diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 9455a76cd..2ea1eca94 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -141,38 +141,38 @@ class _WalletNameFormState extends State { .decorationColor, width: 1.0), ), + suffixIcon: IconButton( + onPressed: () async { + final rName = await generateName(); + FocusManager.instance.primaryFocus?.unfocus(); + + setState(() { + _controller.text = rName; + _walletNewVM.name = rName; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + }); + }, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor, + ), + ), + ), ), validator: WalletNameValidator(), ), - IconButton( - onPressed: () async { - final rName = await generateName(); - FocusManager.instance.primaryFocus?.unfocus(); - - setState(() { - _controller.text = rName; - _walletNewVM.name = rName; - _controller.selection = TextSelection.fromPosition( - TextPosition(offset: _controller.text.length)); - }); - }, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).hintColor, - ), - width: 34, - height: 34, - child: Image.asset( - 'assets/images/refresh_icon.png', - color: Theme.of(context) - .primaryTextTheme - .display1 - .decorationColor, - ), - ), - ), ], ), ), diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 1058380b7..a74c8c290 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -64,33 +64,33 @@ class WalletRestoreFromKeysFromState extends State { controller: nameTextEditingController, hintText: S.of(context).wallet_name, validator: WalletNameValidator(), - ), - IconButton( - onPressed: () async { - final rName = await generateName(); - FocusManager.instance.primaryFocus?.unfocus(); + suffixIcon: IconButton( + onPressed: () async { + final rName = await generateName(); + FocusManager.instance.primaryFocus?.unfocus(); - setState(() { - nameTextEditingController.text = rName; - nameTextEditingController.selection = - TextSelection.fromPosition(TextPosition( - offset: nameTextEditingController.text.length)); - }); - }, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).hintColor, - ), - width: 34, - height: 34, - child: Image.asset( - 'assets/images/refresh_icon.png', - color: Theme.of(context) - .primaryTextTheme - .display1 - .decorationColor, + setState(() { + nameTextEditingController.text = rName; + nameTextEditingController.selection = + TextSelection.fromPosition(TextPosition( + offset: nameTextEditingController.text.length)); + }); + }, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor, + ), ), ), ), diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 07405871c..289d0855d 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -71,16 +71,11 @@ class WalletRestoreFromSeedFormState extends State { BaseTextFormField( controller: nameTextEditingController, hintText: S.of(context).wallet_name, - validator: WalletNameValidator(), - ), - Container( - width: 34, - height: 34, - margin: const EdgeInsets.only(bottom: 15, left: 13), - child: InkWell( - onTap: () async { + suffixIcon: IconButton( + onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); + setState(() { nameTextEditingController.text = rName; nameTextEditingController.selection = @@ -88,17 +83,24 @@ class WalletRestoreFromSeedFormState extends State { offset: nameTextEditingController.text.length)); }); }, - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).hintColor, - borderRadius: BorderRadius.all(Radius.circular(6))), - child: Image.asset('assets/images/refresh_icon.png', - color: Theme.of(context) - .primaryTextTheme - .display1 - .decorationColor)), + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor, + ), + ), ), + validator: WalletNameValidator(), ), ], )), diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index ea67c5d6b..e0d1e8e8d 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -103,10 +103,10 @@ class SeedWidgetState extends State { )), Positioned( top: 0, - right: 0, + right: 8, child: Container( - width: 34, - height: 34, + width: 32, + height: 32, child: InkWell( onTap: () async => _pasteText(), child: Container( From ad0666d4f6f31eace689e27240b3a40330337abd Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 26 Aug 2022 18:28:38 +0300 Subject: [PATCH 36/87] fix south korea flag (#485) --- assets/images/flags/kor.png | Bin 997 -> 1119 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/flags/kor.png b/assets/images/flags/kor.png index 3ce5e45a6f541314e21efe26e6f365e1d0ec2fd1..36e867ea8c5a382c4140e5404889b7457dd6a729 100644 GIT binary patch delta 1048 zcmV+z1n2wZ2j2*gR)6$KL_t(|0nJy>YZE~few$5OL%KEKp(=$$D3pRWg&f*j+lxOy zY(;t#o8n0j+R{J3p1rgue}T3|RIna;6%VE;o-9_pR0}pL81<51T8qVGobS1_PB&Y3 zH+t}cVdlMkGxPSnnQvwc@`jpB;U3>@$A5@;$Yy_kzlqvzM^R@o znFgh^i_)Wx=->4=#zL`mQB`c6k|UVMo;Paujd=YPJK5ge-mtm3X|}Yqn7UCSk#Ouv zrIP0#>$b{9Z+qEzeK$>06pO`bp-}iC#fn{iJDf_T_Ef9Yy!cN5Uu58#?C3h-HMh35 zOv5me6ae{ro`33vj9?%;_=4;X4-d19^+0lMkPLMs$N)1lGwf)0cQ<9TSqg*2#YM{H zawM}y@o3k$sL*g13#0ysD~H^)&yA2=NpqobqT2YoR{P{<7jM-6rq5e+&S zqPVoQ#IoEBH8$HH3sfnWYxL=KntyaC2r3$H~Tn2Wa1^6LjFhd6xB5PfScuUtb@O<+lWumzTi`0*>+;&?(3Za0)3y^pQe6 zeQW10Qu+Cd9rlQ9;o2=KJ$^!+_kYj~}rC5LM+MWZ=!U z+qB_wQh&$R>otxxB8xs_V`E&0OueC^813ljh^?-!ex;CjbTUi(KCIQ~%SI#Dme?lK zYNqtlhZGjQy}f&5vDi16!3s8$L3JYkd4nDlts4FH<_z`k&vKbe9Hr96HZO&+(6h|D z9u(lWMt8%#_z}uAC3iS^!^${vE|#LZ@xxRvY=7$H&BQxPre1^{B-Pq1(`wOV(xUtd=%O`hxFwmR9+Pw}u~kKJHd7H1j}72q>Jdcs;!6Ma*8gUO;fTp?&uQn2G->+AKx(s2p%U}$fJ~fGjOSuYt-_)ddbs7%+AgVN!Vg= za8OK7PuJKKH@=JYPvsiD>L-ID*4Nj$?T4P8o))sNdHQDZ{VhZQDaXwTDZ_GbBN1=p z;)c53C6h@WBgjR_&a!iR3cTCl98!+U2!EDKm`#C{qYwJ3G5mnrevPiHt1B)7LOvBl zy+GQ7qOGkhX201SM4+rxD(}R90{CJ-MVLYrC80KDYNL3P$h7>CUw3=7)dNL~`gpQk zm1lfKvJ4)0KfL;>=c;FMKY86U{dxvFwQ9TVHN8Hcd+xuwhqz(*$p2zY#!n<3tPyOQ S^6zv20000M8En$n*qBr* zm89X}Vc6Q*;_pa5Jw287_V$?TkHO&JARi+djWXe{X_^!a2AOzI{m3>tI?8AqvXjj@ zuF$jDEaY-IE0IXBbUICASWcouP|NcrIxl7xPoJq2gcqz748F{c#p8JwI{=Zg>HehMhS;``ddB;6)TN&NKHB=`xl z9VUds;o2e-Mq1^qbe6h8(Dvjp*UkJP7#rWfDk+g8ywfO}*nr(@3sC-I+SKtv9pSxxVj6CFx^6~HVjGdR-+ya!7Z+PCSBXRH& zEls^uEPwLI-JY9;j%O3Lod||km-*uIV(JN^JlRB#kB^%=TfYv06-za}E{V9X5i0ex zxM_&lwu$!i^fYxQLgo3lTsL#u5MEj4tDP2@zEsOsFUk}J@4te774(lOzSZhl`ogE2 zKvjSU784Vk>`jA$hxOeq_^j2A6(oDUvIgq?A%9*4%A>K-Cf*CvjgxA*a4p5OpGRxWfu;2DAl8*BNe~DTR+mt)@VrQ z;eV`9C@`^^aF5~kbwc=3e??JPxm-2|1_tQ7L_{sS;3H8Bja3Y{PI-YxS1zgHz!h{U zlceUf*L}-E@9ga4GLy+@6@>5=jeJS=_4V Date: Fri, 26 Aug 2022 18:29:00 +0300 Subject: [PATCH 37/87] fix display of Yat alert when address is empty (#484) --- lib/entities/emoji_string_extension.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/entities/emoji_string_extension.dart b/lib/entities/emoji_string_extension.dart index da386dae1..8853c3743 100644 --- a/lib/entities/emoji_string_extension.dart +++ b/lib/entities/emoji_string_extension.dart @@ -16,6 +16,7 @@ extension Emoji on String { bool _hasOnlyEmojis() { final parsedText = this.replaceAll(' ', ''); + if(parsedText.isEmpty) return false; for (final c in Characters(parsedText)) if (!REGEX_EMOJI.hasMatch(c)) return false; return true; From 4a5747c630d566b8401850057912c95da10aaa1e Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 30 Aug 2022 17:01:45 +0300 Subject: [PATCH 38/87] increase brightness for gift card detail page (#478) * increase brightness for gift card detail page * move logic to view model * fix format --- .../screens/ionia/cards/ionia_gift_card_detail_page.dart | 9 ++++++++- .../ionia/ionia_gift_card_details_view_model.dart | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart index 9aa094f4e..dc9746cdf 100644 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; +import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/widgets/framework.dart'; @@ -25,6 +26,8 @@ class IoniaGiftCardDetailPage extends BasePage { final IoniaGiftCardDetailsViewModel viewModel; + + @override Widget leading(BuildContext context) { if (ModalRoute.of(context).isFirst) { @@ -47,7 +50,10 @@ class IoniaGiftCardDetailPage extends BasePage { highlightColor: Colors.transparent, splashColor: Colors.transparent, padding: EdgeInsets.all(0), - onPressed: () => onClose(context), + onPressed: () { + onClose(context); + DeviceDisplayBrightness.setBrightness(viewModel.brightness); + }, child: _backButton), ), ), @@ -64,6 +70,7 @@ class IoniaGiftCardDetailPage extends BasePage { @override Widget body(BuildContext context) { + viewModel.increaseBrightness(); reaction((_) => viewModel.redeemState, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart index e6138bb53..cbbfc49f1 100644 --- a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:mobx/mobx.dart'; +import 'package:device_display_brightness/device_display_brightness.dart'; part 'ionia_gift_card_details_view_model.g.dart'; @@ -14,6 +15,7 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { } final IoniaService ioniaService; + double brightness; @observable IoniaGiftCard giftCard; @@ -32,4 +34,9 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { redeemState = FailureState(e.toString()); } } + + void increaseBrightness() async { + brightness = await DeviceDisplayBrightness.getBrightness(); + await DeviceDisplayBrightness.setBrightness(1.0); + } } \ No newline at end of file From 5fcbf3634be86f21186fe51fda43c687f963882e Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 30 Aug 2022 17:14:05 +0300 Subject: [PATCH 39/87] sort Giftcards alphabetically (#469) --- lib/view_model/ionia/ionia_gift_cards_list_view_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart index f20000a6d..583a4d5ab 100644 --- a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart @@ -87,6 +87,7 @@ abstract class IoniaGiftCardsListViewModelBase with Store { void _getMerchants() { ioniaService.getMerchantsByFilter(categories: selectedFilters).then((value) { + value.sort((a, b) => a.legalName.toLowerCase().compareTo(b.legalName.toLowerCase())); ioniaMerchants = ioniaMerchantList = value; }); } From 7fae9cf9bb1cee6f1e84fb31a595c5baf150b0cc Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 30 Aug 2022 21:03:02 +0300 Subject: [PATCH 40/87] Cw 150 cake pay introduction card (#486) * create introducing card * add ability to close the card * update walletInfo class * update localization * fix intro text * fix card size * show card for existing and new wallet types * disable card for haven wallets * fixes to PR * fixes to PR * fix PR --- cw_core/lib/wallet_info.dart | 17 +++- lib/core/wallet_creation_service.dart | 6 ++ .../dashboard/widgets/balance_page.dart | 17 ++++ lib/src/widgets/introducing_card.dart | 89 +++++++++++++++++++ .../dashboard/balance_view_model.dart | 13 +++ lib/view_model/wallet_creation_vm.dart | 6 +- res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 4 +- res/values/strings_es.arb | 4 +- res/values/strings_fr.arb | 4 +- res/values/strings_hi.arb | 4 +- res/values/strings_hr.arb | 4 +- res/values/strings_it.arb | 4 +- res/values/strings_ja.arb | 4 +- res/values/strings_ko.arb | 4 +- res/values/strings_nl.arb | 4 +- res/values/strings_pl.arb | 4 +- res/values/strings_pt.arb | 4 +- res/values/strings_ru.arb | 4 +- res/values/strings_uk.arb | 4 +- res/values/strings_zh.arb | 4 +- 21 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 lib/src/widgets/introducing_card.dart diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 01bd6ad68..4e614811e 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -9,7 +9,7 @@ part 'wallet_info.g.dart'; class WalletInfo extends HiveObject { WalletInfo(this.id, this.name, this.type, this.isRecovery, this.restoreHeight, this.timestamp, this.dirPath, this.path, this.address, this.yatEid, - this.yatLastUsedAddressRaw) + this.yatLastUsedAddressRaw, this.showIntroCakePayCard) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external( @@ -23,10 +23,11 @@ class WalletInfo extends HiveObject { @required String path, @required String address, String yatEid ='', - String yatLastUsedAddressRaw = ''}) { + String yatLastUsedAddressRaw = '', + bool showIntroCakePayCard}) { return WalletInfo(id, name, type, isRecovery, restoreHeight, date.millisecondsSinceEpoch ?? 0, dirPath, path, address, - yatEid, yatLastUsedAddressRaw); + yatEid, yatLastUsedAddressRaw, showIntroCakePayCard); } static const typeId = 4; @@ -68,6 +69,9 @@ class WalletInfo extends HiveObject { @HiveField(12) String yatLastUsedAddressRaw; + @HiveField(13) + bool showIntroCakePayCard; + String get yatLastUsedAddress => yatLastUsedAddressRaw; set yatLastUsedAddress(String address) { @@ -77,6 +81,13 @@ class WalletInfo extends HiveObject { String get yatEmojiId => yatEid ?? ''; + bool get isShowIntroCakePayCard { + if(showIntroCakePayCard == null) { + return type != WalletType.haven; + } + return showIntroCakePayCard; + } + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp); Stream get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 9c37657f2..1fe36f374 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -46,6 +46,12 @@ class WalletCreationService { .any((walletInfo) => walletInfo.name.toLowerCase() == walletName); } + bool typeExists(WalletType type) { + return walletInfoSource + .values + .any((walletInfo) => walletInfo.type == type); + } + void checkIfExists(String name) { if (exists(name)) { throw Exception('Wallet with name ${name} already exists!'); diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index b3e68e691..3cfa30a9f 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -7,6 +7,9 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/introducing_card.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + class BalancePage extends StatelessWidget{ BalancePage({@required this.dashboardViewModel, @required this.settingsStore}); @@ -44,6 +47,19 @@ class BalancePage extends StatelessWidget{ maxLines: 1, textAlign: TextAlign.center); })), + Observer(builder: (_) { + if (dashboardViewModel.balanceViewModel.isShowCard){ + return IntroducingCard( + title: S.of(context).introducing_cake_pay, + subTitle: S.of(context).cake_pay_learn_more, + borderColor: settingsStore.currentTheme.type == ThemeType.bright + ? Color.fromRGBO(255, 255, 255, 0.2) + : Colors.transparent, + closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard + ); + } + return Container (); + }), Observer(builder: (_) { return ListView.separated( physics: NeverScrollableScrollPhysics(), @@ -180,3 +196,4 @@ class BalancePage extends StatelessWidget{ ); } } + diff --git a/lib/src/widgets/introducing_card.dart b/lib/src/widgets/introducing_card.dart new file mode 100644 index 000000000..2d14465de --- /dev/null +++ b/lib/src/widgets/introducing_card.dart @@ -0,0 +1,89 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/palette.dart'; + +class IntroducingCard extends StatelessWidget { + IntroducingCard( + {this.borderColor, this.closeCard, this.title, this.subTitle}); + + final String title; + final String subTitle; + final Color borderColor; + final Function() closeCard; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16,0,16,16), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: borderColor, + width: 1, + ), + color: Theme.of(context).textTheme.title.backgroundColor), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText(title ?? '', + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.center), + SizedBox(height: 14), + Text(subTitle ?? '', + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1)), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0,16,16,0), + child: GestureDetector( + onTap: closeCard, + child: Container( + height: 23, + width: 23, + decoration: BoxDecoration( + color: Colors.white, shape: BoxShape.circle), + child: Center( + child: Image.asset( + 'assets/images/x.png', + color: Palette.darkBlueCraiola, + height: 15, + width: 15, + )), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 7bd8a9284..aa073206f 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -39,6 +39,7 @@ abstract class BalanceViewModelBase with Store { @required this.fiatConvertationStore}) { isReversing = false; wallet ??= appStore.wallet; + isShowCard = wallet.walletInfo.isShowIntroCakePayCard; reaction((_) => appStore.wallet, _onWalletChange); } @@ -235,6 +236,9 @@ abstract class BalanceViewModelBase with Store { @computed CryptoCurrency get currency => appStore.wallet.currency; + @observable + bool isShowCard; + ReactionDisposer _onCurrentWalletChangeReaction; @action @@ -244,6 +248,15 @@ abstract class BalanceViewModelBase with Store { wallet) { this.wallet = wallet; _onCurrentWalletChangeReaction?.reaction?.dispose(); + isShowCard = wallet.walletInfo.isShowIntroCakePayCard; + } + + @action + Future disableIntroCakePayCard () async { + const cardDisplayStatus = false; + wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus; + await wallet.walletInfo.save(); + isShowCard = cardDisplayStatus; } String _getFiatBalance({double price, String cryptoAmount}) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 50629bb9b..ee563738e 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -37,6 +37,9 @@ abstract class WalletCreationVMBase with Store { bool nameExists(String name) => walletCreationService.exists(name); + bool typeExists(WalletType type) + => walletCreationService.typeExists(type); + Future create({dynamic options}) async { try { state = IsExecutingState(); @@ -56,7 +59,8 @@ abstract class WalletCreationVMBase with Store { restoreHeight: credentials.height ?? 0, date: DateTime.now(), path: path, - dirPath: dirPath); + dirPath: dirPath, + showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven); credentials.walletInfo = walletInfo; final wallet = await process(credentials); walletInfo.address = wallet.walletAddresses.address; diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 839d40d2a..d82b55911 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Geschenkkarte wird generiert", "open_gift_card": "Geschenkkarte öffnen", "contact_support": "Support kontaktieren", - "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden" + "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden", + "introducing_cake_pay": "Einführung von Cake Pay!", + "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 821b1a227..077e81772 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Gift Card is generated", "open_gift_card": "Open Gift Card", "contact_support": "Contact Support", - "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time" + "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time", + "introducing_cake_pay": "Introducing Cake Pay!", + "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 9f4b6d01f..79f70a7b9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Se genera la tarjeta de regalo", "open_gift_card": "Abrir tarjeta de regalo", "contact_support": "Contactar con Soporte", - "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento" + "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento", + "introducing_cake_pay": "¡Presentamos Cake Pay!", + "cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 541f60e56..545583786 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -630,5 +630,7 @@ "gift_card_is_generated": "La carte-cadeau est générée", "open_gift_card": "Ouvrir la carte-cadeau", "contact_support": "Contacter l'assistance", - "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment" + "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", + "introducing_cake_pay": "Présentation de Cake Pay!", + "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 1060ce44e..4d56e3430 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "गिफ्ट कार्ड जनरेट हुआ", "open_gift_card": "गिफ्ट कार्ड खोलें", "contact_support": "सहायता से संपर्क करें", - "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं" + "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं", + "introducing_cake_pay": "परिचय Cake Pay!", + "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4154c812f..db2531d51 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Poklon kartica je generirana", "open_gift_card": "Otvori darovnu karticu", "contact_support": "Kontaktirajte podršku", - "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina" + "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", + "introducing_cake_pay": "Predstavljamo Cake Pay!", + "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 0308f16a9..db0459295 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Il buono regalo è stato generato", "open_gift_card": "Apri carta regalo", "contact_support": "Contatta l'assistenza", - "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento" + "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", + "introducing_cake_pay": "Presentazione di Cake Pay!", + "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 33a9d20cc..c0f70cea7 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "ギフトカードが生成されます", "open_gift_card": "オープンギフトカード", "contact_support": "サポートに連絡する", - "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。" + "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。", + "introducing_cake_pay": "序章Cake Pay!", + "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index edfa14184..da94dc1bb 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "기프트 카드가 생성되었습니다", "open_gift_card": "기프트 카드 열기", "contact_support": "지원팀에 문의", - "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다." + "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", + "introducing_cake_pay": "소개 Cake Pay!", + "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 35d7fccfe..75a7475e5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Cadeaukaart is gegenereerd", "open_gift_card": "Geschenkkaart openen", "contact_support": "Contact opnemen met ondersteuning", - "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin" + "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin", + "introducing_cake_pay": "Introductie van Cake Pay!", + "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 5abf68ff5..d6c626913 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Karta podarunkowa jest generowana", "open_gift_card": "Otwórz kartę podarunkową", "contact_support": "Skontaktuj się z pomocą techniczną", - "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin" + "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin", + "introducing_cake_pay": "Przedstawiamy Ciasto Pay!", + "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3cc7d0319..cdc8e86c0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Cartão presente é gerado", "open_gift_card": "Abrir vale-presente", "contact_support": "Contatar Suporte", - "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento" + "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento", + "introducing_cake_pay": "Apresentando o Cake Pay!", + "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index e4a3d23d1..44dad0323 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -632,5 +632,7 @@ "gift_card_is_generated": "Подарочная карта сгенерирована", "open_gift_card": "Открыть подарочную карту", "contact_support": "Связаться со службой поддержки", - "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin." + "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.", + "introducing_cake_pay": "Представляем Cake Pay!", + "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 171977721..cd5d574a9 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -631,5 +631,7 @@ "gift_card_is_generated": "Подарункова картка створена", "open_gift_card": "Відкрити подарункову картку", "contact_support": "Звернутися до служби підтримки", - "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin" + "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin", + "introducing_cake_pay": "Представляємо Cake Pay!", + "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ccf93bdda..f7375bdfb 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -630,5 +630,7 @@ "gift_card_is_generated": "礼品卡生成", "open_gift_card": "打开礼品卡", "contact_support": "联系支持", - "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡" + "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡", + "introducing_cake_pay": "介绍 Cake Pay!", + "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!" } From d9905a7cf36802f706c80f8d69592960e65d1028 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:34:07 +0300 Subject: [PATCH 41/87] Cw 110 integrate simple swap exchange (#459) * add support for SimpleSwap exchange * fix edit receive amount field * fixed merge conflicts * Fix issues from code review --- .../org.eclipse.buildship.core.prefs | 2 +- assets/images/simpleSwap.png | Bin 0 -> 3016 bytes .../exchange_provider_description.dart | 5 + .../sideshift_exchange_provider.dart | 14 +- .../simpleswap_exchange_provider.dart | 217 ++++++++++++++++++ .../simpleswap/simpleswap_request.dart | 20 ++ .../screens/dashboard/widgets/trade_row.dart | 3 + lib/src/screens/exchange/exchange_page.dart | 2 + .../exchange/exchange_template_page.dart | 4 - .../widgets/present_provider_picker.dart | 3 + lib/store/dashboard/trade_filter_store.dart | 17 +- .../exchange/exchange_trade_view_model.dart | 8 + .../exchange/exchange_view_model.dart | 18 +- lib/view_model/trade_details_view_model.dart | 10 + tool/utils/secret_key.dart | 1 + 15 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 assets/images/simpleSwap.png create mode 100644 lib/exchange/simpleswap/simpleswap_exchange_provider.dart create mode 100644 lib/exchange/simpleswap/simpleswap_request.dart diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index e8895216f..9d2efc8e7 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -1,2 +1,2 @@ connection.project.dir= -eclipse.preferences.version=1 +eclipse.preferences.version=1 \ No newline at end of file diff --git a/assets/images/simpleSwap.png b/assets/images/simpleSwap.png new file mode 100644 index 0000000000000000000000000000000000000000..3c89687a5f867c04a65c8180a0afa381c9fbfabb GIT binary patch literal 3016 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 ki8+u&JovGheeJJ static const sideShift = ExchangeProviderDescription(title: 'SideShift', raw: 3); + + static const simpleSwap = + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4); static ExchangeProviderDescription deserialize({int raw}) { switch (raw) { @@ -24,6 +27,8 @@ class ExchangeProviderDescription extends EnumerableItem return morphToken; case 3: return sideShift; + case 4: + return simpleSwap; default: return null; } diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index c512b6322..1f06de94e 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -47,8 +47,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { if (amount == 0) { return 0.0; } - final fromCurrency = normalizeCryptoCurrency(from); - final toCurrency = normalizeCryptoCurrency(to); + final fromCurrency = _normalizeCryptoCurrency(from); + final toCurrency = _normalizeCryptoCurrency(to); final url = apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency; final response = await get(url); @@ -136,8 +136,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future _createQuote(SideShiftRequest request) async { final url = apiBaseUrl + quotePath; final headers = {'Content-Type': 'application/json'}; - final depositMethod = normalizeCryptoCurrency(request.depositMethod); - final settleMethod = normalizeCryptoCurrency(request.settleMethod); + final depositMethod = _normalizeCryptoCurrency(request.depositMethod); + final settleMethod = _normalizeCryptoCurrency(request.settleMethod); final body = { 'depositMethod': depositMethod, 'settleMethod': settleMethod, @@ -166,8 +166,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { @override Future fetchLimits( {CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async { - final fromCurrency = normalizeCryptoCurrency(from); - final toCurrency = normalizeCryptoCurrency(to); + final fromCurrency = _normalizeCryptoCurrency(from); + final toCurrency = _normalizeCryptoCurrency(to); final url = apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency; final response = await get(url); @@ -247,7 +247,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { @override String get title => 'SideShift'; - static String normalizeCryptoCurrency(CryptoCurrency currency) { + static String _normalizeCryptoCurrency(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.zaddr: return 'zaddr'; diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart new file mode 100644 index 000000000..78e83dd4d --- /dev/null +++ b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart @@ -0,0 +1,217 @@ +import 'dart:convert'; + +import 'package:cake_wallet/exchange/exchange_pair.dart'; +import 'package:cake_wallet/exchange/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; +import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:http/http.dart'; + +class SimpleSwapExchangeProvider extends ExchangeProvider { + SimpleSwapExchangeProvider() + : super( + pairList: CryptoCurrency.all + .map((i) => + CryptoCurrency.all.map((k) => ExchangePair(from: i, to: k, reverse: true)).where((c) => c != null)) + .expand((i) => i) + .toList()); + + static const apiAuthority = 'api.simpleswap.io'; + static const getEstimatePath = '/v1/get_estimated'; + static const rangePath = '/v1/get_ranges'; + static const getExchangePath = '/v1/get_exchange'; + static const createExchangePath = '/v1/create_exchange'; + static const apiKey = secrets.simpleSwapApiKey; + + @override + ExchangeProviderDescription get description => + ExchangeProviderDescription.simpleSwap; + + @override + Future calculateAmount( + {CryptoCurrency from, CryptoCurrency to, double amount, bool isFixedRateMode, bool isReceiveAmount}) async { + try { + if (amount == 0) { + return 0.0; + } + final fromCurrency = _normalizeCryptoCurrency(from); + final toCurrency = _normalizeCryptoCurrency(to); + final params = { + 'api_key': apiKey, + 'currency_from': fromCurrency, + 'currency_to': toCurrency, + 'amount': amount.toString(), + 'fixed': isFixedRateMode.toString() + }; + final uri = Uri.https(apiAuthority, getEstimatePath, params); + + final response = await get(uri); + + if (response.body == null) return 0.00; + final data = json.decode(response.body) as String; + + return double.parse(data); + } catch (_) { + return 0.00; + } + } + + @override + Future checkIsAvailable() async { + final uri = Uri.https(apiAuthority, getEstimatePath, {'api_key': apiKey}); + final response = await get(uri); + + return !(response.statusCode == 403); + } + + @override + Future createTrade({TradeRequest request, bool isFixedRateMode}) async { + final _request = request as SimpleSwapRequest; + final headers = { + 'Content-Type': 'application/json'}; + final params = { + 'api_key': apiKey, + }; + final body = { + "currency_from": _normalizeCryptoCurrency(_request.from), + "currency_to": _normalizeCryptoCurrency(_request.to), + "amount": _request.amount, + "fixed": isFixedRateMode, + "user_refund_address": _request.refundAddress, + "address_to": _request.address + }; + final uri = Uri.https(apiAuthority, createExchangePath, params); + + final response = await post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode != 200) { + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['message'] as String; + + throw TradeNotCreatedException(description, description: error); + } + + throw TradeNotCreatedException(description); + } + + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['id'] as String; + final inputAddress = responseJSON['address_from'] as String; + final settleAddress = responseJSON['user_refund_address'] as String; + + return Trade( + id: id, + provider: description, + from: _request.from, + to: _request.to, + inputAddress: inputAddress, + refundAddress: settleAddress, + state: TradeState.created, + amount: _request.amount, + createdAt: DateTime.now(), + ); + } + + @override + Future fetchLimits({CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async { + final fromCurrency = _normalizeCryptoCurrency(from); + final toCurrency = _normalizeCryptoCurrency(to); + final params = { + 'api_key': apiKey, + 'fixed': isFixedRateMode.toString(), + 'currency_from': fromCurrency, + 'currency_to': toCurrency, + }; + final uri = Uri.https(apiAuthority, rangePath, params); + + final response = await get(uri); + + if (response.statusCode == 500) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['message'] as String; + + throw Exception('$error'); + } + + if (response.statusCode != 200) { + return null; + } + + final responseJSON = json.decode(response.body) as Map; + final min = responseJSON['min'] != null ? double.tryParse(responseJSON['min'] as String) : null; + final max = responseJSON['max'] != null ? double.parse(responseJSON['max'] as String) : null; + + return Limits(min: min, max: max); + } + + @override + Future findTradeById({String id}) async { + final params = {'api_key': apiKey, 'id': id}; + final uri = Uri.https(apiAuthority, getExchangePath, params); + final response = await get(uri); + + if (response.statusCode == 404) { + throw TradeNotFoundException(id, provider: description); + } + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['message'] as String; + + throw TradeNotFoundException(id, provider: description, description: error); + } + + if (response.statusCode != 200) { + return null; + } + + final responseJSON = json.decode(response.body) as Map; + final fromCurrency = responseJSON['currency_from'] as String; + final from = CryptoCurrency.fromString(fromCurrency); + final toCurrency = responseJSON['currency_to'] as String; + final to = CryptoCurrency.fromString(toCurrency); + final inputAddress = responseJSON['address_from'] as String; + final expectedSendAmount = responseJSON['expected_amount'].toString(); + final status = responseJSON['status'] as String; + final state = TradeState.deserialize(raw: status); + + return Trade( + id: id, + from: from, + to: to, + provider: description, + inputAddress: inputAddress, + amount: expectedSendAmount, + state: state, + ); + } + + @override + bool get isAvailable => true; + + @override + String get title => 'SimpleSwap'; + + static String _normalizeCryptoCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.zaddr: + return 'zec'; + case CryptoCurrency.zec: + return 'zec'; + case CryptoCurrency.bnb: + return currency.tag.toLowerCase(); + case CryptoCurrency.usdterc20: + return 'usdterc'; + default: + return currency.title.toLowerCase(); + } + } +} diff --git a/lib/exchange/simpleswap/simpleswap_request.dart b/lib/exchange/simpleswap/simpleswap_request.dart new file mode 100644 index 000000000..03a53c38e --- /dev/null +++ b/lib/exchange/simpleswap/simpleswap_request.dart @@ -0,0 +1,20 @@ +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class SimpleSwapRequest extends TradeRequest { + SimpleSwapRequest({ + @required this.from, + @required this.to, + @required this.address, + @required this.amount, + @required this.refundAddress, + }); + + CryptoCurrency from; + CryptoCurrency to; + String address; + String amount; + String toAmount; + String refundAddress; +} diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index faaac9edb..55b7cadc2 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -90,6 +90,9 @@ class TradeRow extends StatelessWidget { case ExchangeProviderDescription.sideShift: image = Image.asset('assets/images/sideshift.png', width: 36, height: 36); break; + case ExchangeProviderDescription.simpleSwap: + image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36); + break; default: image = null; } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 0c1ae01b3..3fce3029a 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -744,7 +744,9 @@ class ExchangePage extends BasePage { }); _receiveAmountFocus.addListener(() { + if(receiveAmountController.text.isNotEmpty){ exchangeViewModel.isFixedRateMode = true; + } exchangeViewModel.changeReceiveAmount( amount: receiveAmountController.text); }); diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 7d98594d5..5b6f50844 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -1,8 +1,6 @@ import 'dart:ui'; import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -19,9 +17,7 @@ import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; -import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index 16b35d7bb..51738986a 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -74,6 +74,9 @@ class PresentProviderPicker extends StatelessWidget { case ExchangeProviderDescription.sideShift: images.add(Image.asset('assets/images/sideshift.png', width: 20)); break; + case ExchangeProviderDescription.simpleSwap: + images.add(Image.asset('assets/images/simpleSwap.png', width: 20)); + break; } } diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 11ac2a8a4..5cb7fd9e2 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -11,7 +11,9 @@ abstract class TradeFilterStoreBase with Store { TradeFilterStoreBase( {this.displayXMRTO = true, this.displayChangeNow = true, - this.displayMorphToken = true}); + this.displayMorphToken = true, + this.displaySimpleSwap = true, + }); @observable bool displayXMRTO; @@ -22,6 +24,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayMorphToken; + @observable + bool displaySimpleSwap; + @action void toggleDisplayExchange(ExchangeProviderDescription provider) { switch (provider) { @@ -34,13 +39,16 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.morphToken: displayMorphToken = !displayMorphToken; break; + case ExchangeProviderDescription.simpleSwap: + displaySimpleSwap = !displaySimpleSwap; + break; } } List filtered({List trades, WalletBase wallet}) { final _trades = trades.where((item) => item.trade.walletId == wallet.id).toList(); - final needToFilter = !displayChangeNow || !displayXMRTO || !displayMorphToken; + final needToFilter = !displayChangeNow || !displayXMRTO || !displayMorphToken || !displaySimpleSwap; return needToFilter ? _trades @@ -52,7 +60,10 @@ abstract class TradeFilterStoreBase with Store { ExchangeProviderDescription.changeNow) || (displayMorphToken && item.trade.provider == - ExchangeProviderDescription.morphToken)) + ExchangeProviderDescription.morphToken) + ||(displaySimpleSwap && + item.trade.provider == + ExchangeProviderDescription.simpleSwap)) .toList() : _trades; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index de2ac6daa..16523a8ce 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart'; @@ -37,6 +39,12 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.morphToken: _provider = MorphTokenExchangeProvider(trades: trades); break; + case ExchangeProviderDescription.sideShift: + _provider = SideShiftExchangeProvider(); + break; + case ExchangeProviderDescription.simpleSwap: + _provider = SimpleSwapExchangeProvider(); + break; } items = ObservableList(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 3eed44011..f0ff5ee9e 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; +import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -35,7 +37,7 @@ abstract class ExchangeViewModelBase with Store { this.tradesStore, this._settingsStore) { const excludeDepositCurrencies = [CryptoCurrency.xhv]; const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv]; - providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider()]; + providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()]; _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -267,6 +269,18 @@ abstract class ExchangeViewModelBase with Store { currency = depositCurrency; } + if (provider is SimpleSwapExchangeProvider) { + request = SimpleSwapRequest( + from: depositCurrency, + to: receiveCurrency, + amount: depositAmount?.replaceAll(',', '.'), + address: receiveAddress, + refundAddress: depositAddress, + ); + amount = depositAmount; + currency = depositCurrency; + } + if (provider is XMRTOExchangeProvider) { request = XMRTOTradeRequest( from: depositCurrency, @@ -459,6 +473,6 @@ abstract class ExchangeViewModelBase with Store { isReceiveAmountEditable = false; }*/ //isReceiveAmountEditable = false; - isReceiveAmountEditable = provider is ChangeNowExchangeProvider; + isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider; } } diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 043028c90..00aabdebd 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; @@ -35,6 +36,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.sideShift: _provider = SideShiftExchangeProvider(); break; + case ExchangeProviderDescription.simpleSwap: + _provider = SimpleSwapExchangeProvider(); + break; } items = ObservableList(); @@ -112,6 +116,12 @@ abstract class TradeDetailsViewModelBase with Store { title: 'Track', value: buildURL, onTap: () => launch(buildURL))); } + if (trade.provider == ExchangeProviderDescription.simpleSwap) { + final buildURL = 'https://simpleswap.io/exchange?id=${trade.id.toString()}'; + items.add(TrackTradeListItem( + title: 'Track', value: buildURL, onTap: () => launch(buildURL))); + } + if (trade.createdAt != null) { items.add(StandartListItem( title: S.current.trade_details_created_at, diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index aa9928c8c..9bf360017 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -25,6 +25,7 @@ class SecretKey { SecretKey('moonPaySecretKey', () => ''), SecretKey('sideShiftAffiliateId', () => ''), SecretKey('sideShiftApiKey', () => ''), + SecretKey('simpleSwapApiKey', () => '') ]; final String name; From 92458e2f4bdcd71e30036a57623996306490bd31 Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:37:43 +0100 Subject: [PATCH 42/87] Add platform device id package. Send device uuid to ionia for purchase request. (#494) --- ios/Podfile.lock | 12 ++++++++++++ lib/ionia/ionia_api.dart | 3 +++ lib/ionia/ionia_service.dart | 3 +++ lib/src/screens/ionia/cards/ionia_account_page.dart | 2 +- pubspec_base.yaml | 1 + 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index af603c115..66762582e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -63,6 +63,8 @@ PODS: - Flutter - device_display_brightness (0.0.1): - Flutter + - device_info (0.0.1): + - Flutter - devicelocale (0.0.1): - Flutter - DKImagePickerController/Core (4.3.2): @@ -113,6 +115,8 @@ PODS: - Flutter - "permission_handler (5.1.0+2)": - Flutter + - platform_device_id (0.0.1): + - Flutter - Reachability (3.2) - SDWebImage (5.9.1): - SDWebImage/Core (= 5.9.1) @@ -141,6 +145,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) + - device_info (from `.symlinks/plugins/device_info/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -150,6 +155,7 @@ DEPENDENCIES: - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) @@ -183,6 +189,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" + device_info: + :path: ".symlinks/plugins/device_info/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: @@ -201,6 +209,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider/ios" permission_handler: :path: ".symlinks/plugins/permission_handler/ios" + platform_device_id: + :path: ".symlinks/plugins/platform_device_id/ios" share: :path: ".symlinks/plugins/share/ios" shared_preferences: @@ -221,6 +231,7 @@ SPEC CHECKSUMS: cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 + device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 @@ -233,6 +244,7 @@ SPEC CHECKSUMS: package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 + platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart index ca3eae03d..bca3221be 100644 --- a/lib/ionia/ionia_api.dart +++ b/lib/ionia/ionia_api.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/ionia/ionia_gift_card.dart'; class IoniaApi { static const baseUri = 'api.ionia.io'; static const pathPrefix = 'cake'; + static const requestedUUIDHeader = 'requestedUUID'; static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser'); static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail'); static final signInUri = Uri.https(baseUri, '/$pathPrefix/SignIn'); @@ -254,6 +255,7 @@ class IoniaApi { // Purchase Gift Card Future purchaseGiftCard({ + @required String requestedUUID, @required String merchId, @required double amount, @required String currency, @@ -264,6 +266,7 @@ class IoniaApi { 'clientId': clientId, 'username': username, 'password': password, + requestedUUIDHeader: requestedUUID, 'Content-Type': 'application/json'}; final body = { 'Amount': amount, diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart index d1a2052f0..da924b0ed 100644 --- a/lib/ionia/ionia_service.dart +++ b/lib/ionia/ionia_service.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:platform_device_id/platform_device_id.dart'; class IoniaService { IoniaService(this.secureStorage, this.ioniaApi); @@ -114,7 +115,9 @@ class IoniaService { @required String currency}) async { final username = await secureStorage.read(key: ioniaUsernameStorageKey); final password = await secureStorage.read(key: ioniaPasswordStorageKey); + final deviceId = await PlatformDeviceId.getDeviceId; return ioniaApi.purchaseGiftCard( + requestedUUID: deviceId, merchId: merchId, amount: amount, currency: currency, diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart index 28e7e9b42..817f966fa 100644 --- a/lib/src/screens/ionia/cards/ionia_account_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_page.dart @@ -124,7 +124,7 @@ class IoniaAccountPage extends BasePage { //), SizedBox(height: 40), Observer( - builder: (_) => IoniaTile(title: S.of(context).email_address, subTitle: ioniaAccountViewModel.email), + builder: (_) => IoniaTile(title: S.of(context).email_address, subTitle: ioniaAccountViewModel.email ?? ''), ), Divider() ], diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 83d461546..89f745856 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -55,6 +55,7 @@ dependencies: unorm_dart: ^0.2.0 permission_handler: ^5.0.1+1 device_display_brightness: ^0.0.6 + platform_device_id: ^0.2.1 dev_dependencies: flutter_test: From c50eeee58b417768a60c82281b25a9309dc25011 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 1 Sep 2022 16:12:38 +0200 Subject: [PATCH 43/87] CW 68 exchange automatic rate selector (#472) * Add 'Exchange provider picker' Save user selections * Save user's exchange providers selection * Add text for selected providers availability * Fix selected providers not updating * Load limits based on highest maximum in the selected providers * Change received and deposit amount to be the best value from the selected providers * Add provider name next to Trade ID Set selected provider based on amount calculated * Grey out providers who doesn't support selected currency pair * Fix disabled providers * Add Provider logo in Confirm Screen * Only choose a provider if it satisfies its limits * Fix amount validation * Fix typo in error message * Add a queue of possible exchange providers sorted by the best rate to try next if one failed * Fix string locale typo * Add Localization for other languages * Add Placeholder text when there are no providers selected * Check Exchange provider availability before creating a trade * Fix "Fixed Rate" changing unconditionally * Enable "convert to" field regardless of the provider * Remove "Choose one" from providers picker * Merge Master * Fix Conflicts with master * Add missing isEnabled field in simple swap provider --- lib/di.dart | 4 +- lib/entities/preferences_key.dart | 2 + .../changenow_exchange_provider.dart | 3 + lib/exchange/exchange_provider.dart | 1 + .../exchange_provider_description.dart | 19 +- .../morphtoken_exchange_provider.dart | 3 + .../sideshift_exchange_provider.dart | 3 + .../simpleswap_exchange_provider.dart | 3 + .../xmrto/xmrto_exchange_provider.dart | 3 + lib/src/screens/exchange/exchange_page.dart | 26 +- .../widgets/present_provider_picker.dart | 73 ++-- .../exchange_trade/exchange_confirm_page.dart | 21 +- lib/src/widgets/check_box_picker.dart | 166 ++++++++ .../exchange/exchange_trade_view_model.dart | 2 +- .../exchange/exchange_view_model.dart | 382 +++++++++++------- res/values/strings_de.arb | 11 +- res/values/strings_en.arb | 9 +- res/values/strings_es.arb | 9 +- res/values/strings_fr.arb | 9 +- res/values/strings_hi.arb | 9 +- res/values/strings_hr.arb | 9 +- res/values/strings_it.arb | 9 +- res/values/strings_ja.arb | 37 +- res/values/strings_ko.arb | 9 +- res/values/strings_nl.arb | 11 +- res/values/strings_pl.arb | 9 +- res/values/strings_pt.arb | 9 +- res/values/strings_ru.arb | 9 +- res/values/strings_uk.arb | 9 +- res/values/strings_zh.arb | 9 +- 30 files changed, 627 insertions(+), 251 deletions(-) create mode 100644 lib/src/widgets/check_box_picker.dart diff --git a/lib/di.dart b/lib/di.dart index bd911335d..081aec364 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -485,7 +485,9 @@ Future setup( _tradesSource, getIt.get(), getIt.get(), - getIt.get().settingsStore)); + getIt.get().settingsStore, + getIt.get(), + )); getIt.registerFactory(() => ExchangeTradeViewModel( wallet: getIt.get().wallet, diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index f4a0008a2..6cf7e5608 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -26,4 +26,6 @@ class PreferencesKey { static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; + + static const exchangeProvidersSelection = 'exchange-providers-selection'; } diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index c6594516d..df5afe5ab 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -39,6 +39,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.changeNow; diff --git a/lib/exchange/exchange_provider.dart b/lib/exchange/exchange_provider.dart index 30a359fda..a960c0ad7 100644 --- a/lib/exchange/exchange_provider.dart +++ b/lib/exchange/exchange_provider.dart @@ -13,6 +13,7 @@ abstract class ExchangeProvider { List pairList; ExchangeProviderDescription description; bool get isAvailable; + bool get isEnabled; @override String toString() => title; diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 7b3509fe1..651d0502e 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -2,20 +2,23 @@ import 'package:cw_core/enumerable_item.dart'; class ExchangeProviderDescription extends EnumerableItem with Serializable { - const ExchangeProviderDescription({String title, int raw}) + const ExchangeProviderDescription({String title, int raw, this.horizontalLogo = false, this.image}) : super(title: title, raw: raw); - static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0); + final bool horizontalLogo; + final String image; + + static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png'); static const changeNow = - ExchangeProviderDescription(title: 'ChangeNOW', raw: 1); + ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png'); static const morphToken = - ExchangeProviderDescription(title: 'MorphToken', raw: 2); + ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); static const sideShift = - ExchangeProviderDescription(title: 'SideShift', raw: 3); - - static const simpleSwap = - ExchangeProviderDescription(title: 'SimpleSwap', raw: 4); + ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); + + static const simpleSwap = + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); static ExchangeProviderDescription deserialize({int raw}) { switch (raw) { diff --git a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart b/lib/exchange/morphtoken/morphtoken_exchange_provider.dart index cea92a1bb..4b3e6f646 100644 --- a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart +++ b/lib/exchange/morphtoken/morphtoken_exchange_provider.dart @@ -63,6 +63,9 @@ class MorphTokenExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.morphToken; diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index 1f06de94e..878ffd528 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -244,6 +244,9 @@ class SideShiftExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override String get title => 'SideShift'; diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart index 78e83dd4d..cf6fb3d38 100644 --- a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart +++ b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart @@ -197,6 +197,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override String get title => 'SimpleSwap'; diff --git a/lib/exchange/xmrto/xmrto_exchange_provider.dart b/lib/exchange/xmrto/xmrto_exchange_provider.dart index 12c6c8587..8f08d5a5b 100644 --- a/lib/exchange/xmrto/xmrto_exchange_provider.dart +++ b/lib/exchange/xmrto/xmrto_exchange_provider.dart @@ -44,6 +44,9 @@ class XMRTOExchangeProvider extends ExchangeProvider { @override bool get isAvailable => _isAvailable; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.xmrto; diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 3fce3029a..7e4aa6089 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -354,8 +354,12 @@ class ExchangePage extends BasePage { padding: EdgeInsets.only(bottom: 15), child: Observer(builder: (_) { final description = exchangeViewModel.isFixedRateMode + ? exchangeViewModel.isAvailableInSelected ? S.of(context).amount_is_guaranteed - : S.of(context).amount_is_estimate; + : S.of(context).fixed_pair_not_supported + : exchangeViewModel.isAvailableInSelected + ? S.of(context).amount_is_estimate + : S.of(context).variable_pair_not_supported; return Center( child: Text( description, @@ -399,8 +403,8 @@ class ExchangePage extends BasePage { }, color: Theme.of(context).accentTextTheme.body2.color, textColor: Colors.white, - isLoading: - exchangeViewModel.tradeState is TradeIsCreating)), + isDisabled: exchangeViewModel.selectedProviders.isEmpty, + isLoading: exchangeViewModel.tradeState is TradeIsCreating)), ]), )), )); @@ -518,13 +522,6 @@ class ExchangePage extends BasePage { exchangeViewModel.changeReceiveCurrency( currency: CryptoCurrency.fromString(template.receiveCurrency)); - switch (template.provider) { - case 'ChangeNOW': - exchangeViewModel.changeProvider( - provider: exchangeViewModel.providerList[0]); - break; - } - exchangeViewModel.changeDepositAmount(amount: template.amount); exchangeViewModel.depositAddress = template.depositAddress; exchangeViewModel.receiveAddress = template.receiveAddress; @@ -744,11 +741,10 @@ class ExchangePage extends BasePage { }); _receiveAmountFocus.addListener(() { - if(receiveAmountController.text.isNotEmpty){ - exchangeViewModel.isFixedRateMode = true; - } - exchangeViewModel.changeReceiveAmount( - amount: receiveAmountController.text); + if (_receiveAmountFocus.hasFocus) { + exchangeViewModel.isFixedRateMode = true; + } + exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); }); _depositAmountFocus.addListener(() { diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index 51738986a..cd3653ee5 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -1,11 +1,9 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/check_box_picker.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; class PresentProviderPicker extends StatelessWidget { @@ -38,11 +36,16 @@ class PresentProviderPicker extends StatelessWidget { fontWeight: FontWeight.w600, color: Colors.white)), Observer( - builder: (_) => Text('${exchangeViewModel.provider.title}', - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.headline.color))) + builder: (_) => Text( + exchangeViewModel.selectedProviders.isEmpty + ? S.of(context).choose_one + : exchangeViewModel.selectedProviders.length > 1 + ? S.of(context).automatic + : exchangeViewModel.selectedProviders.first.title, + style: TextStyle( + fontSize: 10.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.headline.color))) ], ), SizedBox(width: 5), @@ -54,41 +57,19 @@ class PresentProviderPicker extends StatelessWidget { )); } - void _presentProviderPicker(BuildContext context) { - final items = exchangeViewModel.providersForCurrentPair(); - final selectedItem = items.indexOf(exchangeViewModel.provider); - final images = []; - String description; - - for (var provider in items) { - switch (provider.description) { - case ExchangeProviderDescription.xmrto: - images.add(Image.asset('assets/images/xmr_btc.png')); - break; - case ExchangeProviderDescription.changeNow: - images.add(Image.asset('assets/images/change_now.png')); - break; - case ExchangeProviderDescription.morphToken: - images.add(Image.asset('assets/images/morph_icon.png')); - break; - case ExchangeProviderDescription.sideShift: - images.add(Image.asset('assets/images/sideshift.png', width: 20)); - break; - case ExchangeProviderDescription.simpleSwap: - images.add(Image.asset('assets/images/simpleSwap.png', width: 20)); - break; - } - } - - showPopUp( - builder: (BuildContext popUpContext) => Picker( - items: items, - images: images, - selectedAtIndex: selectedItem, + void _presentProviderPicker(BuildContext context) async { + await showPopUp( + builder: (BuildContext popUpContext) => CheckBoxPicker( + items: exchangeViewModel.providerList + .map((e) => CheckBoxItem( + e.title, + exchangeViewModel.selectedProviders.contains(e), + isDisabled: !exchangeViewModel.providersForCurrentPair().contains(e), + )) + .toList(), title: S.of(context).change_exchange_provider, - description: description, - onItemSelected: (ExchangeProvider provider) { - if (!provider.isAvailable) { + onChanged: (int index, bool value) { + if (!exchangeViewModel.providerList[index].isAvailable) { showPopUp( builder: (BuildContext popUpContext) => AlertWithOneAction( alertTitle: 'Error', @@ -98,8 +79,14 @@ class PresentProviderPicker extends StatelessWidget { context: context); return; } - exchangeViewModel.changeProvider(provider: provider); + if (value) { + exchangeViewModel.addExchangeProvider(exchangeViewModel.providerList[index]); + } else { + exchangeViewModel.removeExchangeProvider(exchangeViewModel.providerList[index]); + } }), context: context); + + exchangeViewModel.saveSelectedProviders(); } } diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 8fd4883df..661ed95a3 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:flutter/material.dart'; @@ -56,7 +57,7 @@ class ExchangeConfirmPage extends BasePage { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - S.of(context).trade_id, + "${trade.provider.title} ${S.of(context).trade_id}", style: TextStyle( fontSize: 12.0, fontWeight: FontWeight.w500, @@ -101,7 +102,23 @@ class ExchangeConfirmPage extends BasePage { ], ), ), - Flexible(child: Offstage()), + Flexible( + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (trade.provider.image?.isNotEmpty ?? false) + ? Image.asset(trade.provider.image, height: 50) + : const SizedBox(), + if (!trade.provider.horizontalLogo) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(trade.provider.title), + ), + ], + ), + ), + ), ], )), PrimaryButton( diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart new file mode 100644 index 000000000..b0d435caf --- /dev/null +++ b/lib/src/widgets/check_box_picker.dart @@ -0,0 +1,166 @@ +import 'dart:ui'; +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class CheckBoxPicker extends StatefulWidget { + CheckBoxPicker({ + @required this.items, + @required this.onChanged, + this.title, + this.displayItem, + this.isSeparated = true, + }); + + final List items; + final String title; + final Widget Function(CheckBoxItem) displayItem; + final bool isSeparated; + final Function(int, bool) onChanged; + + @override + CheckBoxPickerState createState() => CheckBoxPickerState(items); +} + +class CheckBoxPickerState extends State { + CheckBoxPickerState(this.items); + + final List items; + + ScrollController controller = ScrollController(); + + @override + Widget build(BuildContext context) { + return AlertBackground( + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.title?.isNotEmpty ?? false) + Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + widget.title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + color: Colors.white, + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 24, right: 24, top: 24), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + color: Theme.of(context).accentTextTheme.title.color, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.65, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Stack( + alignment: Alignment.center, + children: [ + (items?.length ?? 0) > 3 + ? Scrollbar( + controller: controller, + child: itemsList(), + ) + : itemsList(), + ], + ), + ), + ], + ), + ), + ), + ), + ) + ], + ), + AlertCloseButton(), + ], + ), + ); + } + + Widget itemsList() { + return Container( + color: Theme.of(context).accentTextTheme.headline6.backgroundColor, + child: ListView.separated( + padding: EdgeInsets.zero, + controller: controller, + shrinkWrap: true, + separatorBuilder: (context, index) => widget.isSeparated + ? Divider( + color: Theme.of(context).accentTextTheme.title.backgroundColor, + height: 1, + ) + : const SizedBox(), + itemCount: items == null || items.isEmpty ? 0 : items.length, + itemBuilder: (context, index) => buildItem(index), + ), + ); + } + + Widget buildItem(int index) { + final item = items[index]; + + return GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + height: 55, + color: Theme.of(context).accentTextTheme.headline6.color, + padding: EdgeInsets.only(left: 24, right: 24), + child: CheckboxListTile( + value: item.value, + activeColor: item.value + ? Palette.blueCraiola + : Theme.of(context).accentTextTheme.subhead.decorationColor, + checkColor: Colors.white, + title: widget.displayItem?.call(item) ?? + Text( + item.title, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: item.isDisabled + ? Colors.grey.withOpacity(0.5) + : Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ), + onChanged: (bool value) { + item.value = value; + widget.onChanged(index, value); + setState(() {}); + }, + controlAffinity: ListTileControlAffinity.leading, + ), + ), + ); + } +} + +class CheckBoxItem { + CheckBoxItem(this.title, this.value, {this.isDisabled = false}); + + final String title; + final bool isDisabled; + bool value; +} diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 16523a8ce..6ca7ebeda 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -116,7 +116,7 @@ abstract class ExchangeTradeViewModelBase with Store { items?.clear(); items.add(ExchangeTradeItem( - title: S.current.id, data: '${trade.id}', isCopied: true)); + title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true)); if (trade.extraId != null) { final title = trade.from == CryptoCurrency.xrp diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index f0ff5ee9e..d91c393ab 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1,3 +1,7 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; @@ -27,6 +31,7 @@ import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dar import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart'; import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'exchange_view_model.g.dart'; @@ -34,10 +39,24 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, - this.tradesStore, this._settingsStore) { + this.tradesStore, this._settingsStore, this.sharedPreferences) { const excludeDepositCurrencies = [CryptoCurrency.xhv]; const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv]; providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()]; + + currentTradeAvailableProviders = SplayTreeMap(); + + final Map exchangeProvidersSelection = json + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + + /// if the provider is not in the user settings (user's first time or newly added provider) + /// then use its default value decided by us + selectedProviders = ObservableList.of(providerList.where( + (element) => exchangeProvidersSelection[element.title] == null + ? element.isEnabled + : (exchangeProvidersSelection[element.title] as bool)) + .toList()); + _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -48,7 +67,7 @@ abstract class ExchangeViewModelBase with Store { ? wallet.walletAddresses.address : ''; limitsState = LimitsInitialState(); tradeState = ExchangeTradeStateInitial(); - _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12; + _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = wallet.type == WalletType.bitcoin ? 8 : 12; provider = providersForCurrentPair().first; final initialProvider = provider; provider.checkIsAvailable().then((bool isAvailable) { @@ -79,10 +98,20 @@ abstract class ExchangeViewModelBase with Store { final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; final TradesStore tradesStore; + final SharedPreferences sharedPreferences; @observable ExchangeProvider provider; + /// Maps in dart are not sorted by default + /// SplayTreeMap is a map sorted by keys + /// will use it to sort available providers + /// depending on the amount they yield for the current trade + SplayTreeMap currentTradeAvailableProviders; + + @observable + ObservableList selectedProviders; + @observable List providerList; @@ -147,17 +176,7 @@ abstract class ExchangeViewModelBase with Store { NumberFormat _cryptoNumberFormat; - SettingsStore _settingsStore; - - @action - void changeProvider({ExchangeProvider provider}) { - this.provider = provider; - depositAmount = ''; - receiveAmount = ''; - isFixedRateMode = false; - _defineIsReceiveAmountEditable(); - loadLimits(); - } + final SettingsStore _settingsStore; @action void changeDepositCurrency({CryptoCurrency currency}) { @@ -188,20 +207,46 @@ abstract class ExchangeViewModelBase with Store { return; } - final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.parse(amount.replaceAll(',', '.')) ?? 0; - provider - .calculateAmount( - from: receiveCurrency, - to: depositCurrency, - amount: _amount, - isFixedRateMode: isFixedRateMode, - isReceiveAmount: true) - .then((amount) => _cryptoNumberFormat + currentTradeAvailableProviders.clear(); + for (var provider in selectedProviders) { + provider + .calculateAmount( + from: receiveCurrency, + to: depositCurrency, + amount: _enteredAmount, + isFixedRateMode: isFixedRateMode, + isReceiveAmount: true) + .then((amount) { + + final from = isFixedRateMode + ? receiveCurrency + : depositCurrency; + final to = isFixedRateMode + ? depositCurrency + : receiveCurrency; + + provider.fetchLimits( + from: from, + to: to, + isFixedRateMode: isFixedRateMode, + ).then((limits) { + /// if the entered amount doesn't exceed the limits of this provider + if ((limits.max ?? double.maxFinite) >= _enteredAmount + && (limits.min ?? 0) <= _enteredAmount) { + /// add this provider as its valid for this trade + /// will be sorted ascending already since + /// we seek the least deposit amount + currentTradeAvailableProviders[amount] = provider; + } + return amount; + }).then((amount) => depositAmount = _cryptoNumberFormat .format(amount) .toString() - .replaceAll(RegExp('\\,'), '')) - .then((amount) => depositAmount = amount); + .replaceAll(RegExp('\\,'), '')); + }); + } } @action @@ -215,23 +260,56 @@ abstract class ExchangeViewModelBase with Store { return; } - final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0; - provider - .calculateAmount( - from: depositCurrency, - to: receiveCurrency, - amount: _amount, - isFixedRateMode: isFixedRateMode, - isReceiveAmount: false) - .then((amount) => _cryptoNumberFormat + final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + currentTradeAvailableProviders.clear(); + for (var provider in selectedProviders) { + provider + .calculateAmount( + from: depositCurrency, + to: receiveCurrency, + amount: _enteredAmount, + isFixedRateMode: isFixedRateMode, + isReceiveAmount: false) + .then((amount) { + + final from = isFixedRateMode + ? receiveCurrency + : depositCurrency; + final to = isFixedRateMode + ? depositCurrency + : receiveCurrency; + + provider.fetchLimits( + from: from, + to: to, + isFixedRateMode: isFixedRateMode, + ).then((limits) { + + /// if the entered amount doesn't exceed the limits of this provider + if ((limits.max ?? double.maxFinite) >= _enteredAmount + && (limits.min ?? 0) <= _enteredAmount) { + /// add this provider as its valid for this trade + /// subtract from maxFinite so the provider + /// with the largest amount would be sorted ascending + currentTradeAvailableProviders[double.maxFinite - amount] = provider; + } + return amount; + }).then((amount) => receiveAmount = + receiveAmount = _cryptoNumberFormat .format(amount) .toString() - .replaceAll(RegExp('\\,'), '')) - .then((amount) => receiveAmount = amount); + .replaceAll(RegExp('\\,'), '')); + }); + } } @action Future loadLimits() async { + if (selectedProviders.isEmpty) { + return; + } + limitsState = LimitsIsLoading(); try { @@ -241,10 +319,29 @@ abstract class ExchangeViewModelBase with Store { final to = isFixedRateMode ? depositCurrency : receiveCurrency; - limits = await provider.fetchLimits( + + limits = await selectedProviders.first.fetchLimits( from: from, to: to, isFixedRateMode: isFixedRateMode); + + /// if the first provider limits is bounded then check with other providers + /// for the highest maximum limit + if (limits.max != null) { + for (int i = 1;i < selectedProviders.length;i++) { + final Limits tempLimits = await selectedProviders[i].fetchLimits( + from: from, + to: to, + isFixedRateMode: isFixedRateMode); + + /// set the limits with the maximum provider limit + /// if there is a provider with null max then it's the maximum limit + if ((tempLimits.max ?? double.maxFinite) > limits.max) { + limits = tempLimits; + } + } + } + limitsState = LimitsLoadedSuccessfully(limits: limits); } catch (e) { limitsState = LimitsLoadedFailure(error: e.toString()); @@ -255,102 +352,97 @@ abstract class ExchangeViewModelBase with Store { Future createTrade() async { TradeRequest request; String amount; - CryptoCurrency currency; - if (provider is SideShiftExchangeProvider) { - request = SideShiftRequest( + for (var provider in currentTradeAvailableProviders.values) { + if (!(await provider.checkIsAvailable())) { + continue; + } + + if (provider is SideShiftExchangeProvider) { + request = SideShiftRequest( depositMethod: depositCurrency, settleMethod: receiveCurrency, depositAmount: depositAmount?.replaceAll(',', '.'), settleAddress: receiveAddress, refundAddress: depositAddress, - ); - amount = depositAmount; - currency = depositCurrency; - } + ); + amount = depositAmount; + } - if (provider is SimpleSwapExchangeProvider) { - request = SimpleSwapRequest( + if (provider is SimpleSwapExchangeProvider) { + request = SimpleSwapRequest( from: depositCurrency, to: receiveCurrency, amount: depositAmount?.replaceAll(',', '.'), address: receiveAddress, refundAddress: depositAddress, - ); - amount = depositAmount; - currency = depositCurrency; - } + ); + amount = depositAmount; + } - if (provider is XMRTOExchangeProvider) { - request = XMRTOTradeRequest( - from: depositCurrency, - to: receiveCurrency, - amount: depositAmount?.replaceAll(',', '.'), - receiveAmount: receiveAmount?.replaceAll(',', '.'), - address: receiveAddress, - refundAddress: depositAddress, - isBTCRequest: isReceiveAmountEntered); - amount = depositAmount; - currency = depositCurrency; - } + if (provider is XMRTOExchangeProvider) { + request = XMRTOTradeRequest( + from: depositCurrency, + to: receiveCurrency, + amount: depositAmount?.replaceAll(',', '.'), + receiveAmount: receiveAmount?.replaceAll(',', '.'), + address: receiveAddress, + refundAddress: depositAddress, + isBTCRequest: isReceiveAmountEntered); + amount = depositAmount; + } - if (provider is ChangeNowExchangeProvider) { - request = ChangeNowRequest( - from: depositCurrency, - to: receiveCurrency, - fromAmount: depositAmount?.replaceAll(',', '.'), - toAmount: receiveAmount?.replaceAll(',', '.'), - refundAddress: depositAddress, - address: receiveAddress, - isReverse: isReverse); - amount = isReverse ? receiveAmount : depositAmount; - currency = depositCurrency; - } + if (provider is ChangeNowExchangeProvider) { + request = ChangeNowRequest( + from: depositCurrency, + to: receiveCurrency, + fromAmount: depositAmount?.replaceAll(',', '.'), + toAmount: receiveAmount?.replaceAll(',', '.'), + refundAddress: depositAddress, + address: receiveAddress, + isReverse: isReverse); + amount = isReverse ? receiveAmount : depositAmount; + } - if (provider is MorphTokenExchangeProvider) { - request = MorphTokenRequest( - from: depositCurrency, - to: receiveCurrency, - amount: depositAmount?.replaceAll(',', '.'), - refundAddress: depositAddress, - address: receiveAddress); - amount = depositAmount; - currency = depositCurrency; - } + if (provider is MorphTokenExchangeProvider) { + request = MorphTokenRequest( + from: depositCurrency, + to: receiveCurrency, + amount: depositAmount?.replaceAll(',', '.'), + refundAddress: depositAddress, + address: receiveAddress); + amount = depositAmount; + } - amount = amount.replaceAll(',', '.'); + amount = amount.replaceAll(',', '.'); - if (limitsState is LimitsLoadedSuccessfully && amount != null) { - if (double.parse(amount) < limits.min) { - tradeState = TradeIsCreatedFailure( - title: provider.title, - error: S.current.error_text_minimal_limit('${provider.description}', - '${limits.min}', currency.toString())); - } else if (limits.max != null && double.parse(amount) > limits.max) { - tradeState = TradeIsCreatedFailure( - title: provider.title, - error: S.current.error_text_maximum_limit('${provider.description}', - '${limits.max}', currency.toString())); - } else { - try { - tradeState = TradeIsCreating(); - final trade = await provider.createTrade( - request: request, isFixedRateMode: isFixedRateMode); - trade.walletId = wallet.id; - tradesStore.setTrade(trade); - await trades.add(trade); - tradeState = TradeIsCreatedSuccessfully(trade: trade); - } catch (e) { - tradeState = - TradeIsCreatedFailure(title: provider.title, error: e.toString()); + if (limitsState is LimitsLoadedSuccessfully && amount != null) { + if (double.parse(amount) < limits.min) { + continue; + } else if (limits.max != null && double.parse(amount) > limits.max) { + continue; + } else { + try { + tradeState = TradeIsCreating(); + final trade = await provider.createTrade( + request: request, isFixedRateMode: isFixedRateMode); + trade.walletId = wallet.id; + tradesStore.setTrade(trade); + await trades.add(trade); + tradeState = TradeIsCreatedSuccessfully(trade: trade); + /// return after the first successful trade + return; + } catch (e) { + continue; + } } } - } else { - tradeState = TradeIsCreatedFailure( - title: provider.title, - error: S.current - .error_text_limits_loading_failed('${provider.description}')); } + + /// if the code reached here then none of the providers succeeded + tradeState = TradeIsCreatedFailure( + title: S.current.trade_not_created, + error: S.current.none_of_selected_providers_can_exchange); } @action @@ -414,7 +506,7 @@ abstract class ExchangeViewModelBase with Store { final providers = providerList .where((provider) => provider.pairList .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) + pair.from == (from ?? depositCurrency) && pair.to == (to ?? receiveCurrency)) .isNotEmpty) .toList(); @@ -422,27 +514,8 @@ abstract class ExchangeViewModelBase with Store { } void _onPairChange() { - final isPairExist = provider.pairList - .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) - .isNotEmpty; - - if (isPairExist) { - final provider = - _providerForPair(from: depositCurrency, to: receiveCurrency); - - if (provider != null) { - changeProvider(provider: provider); - } - } else { - depositAmount = ''; - receiveAmount = ''; - } - } - - ExchangeProvider _providerForPair({CryptoCurrency from, CryptoCurrency to}) { - final providers = _providersForPair(from: from, to: to); - return providers.isNotEmpty ? providers[0] : null; + depositAmount = ''; + receiveAmount = ''; } void _initialPairBasedOnWallet() { @@ -473,6 +546,45 @@ abstract class ExchangeViewModelBase with Store { isReceiveAmountEditable = false; }*/ //isReceiveAmountEditable = false; - isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider; + // isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider); + // isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider; + isReceiveAmountEditable = true; + } + + @action + void addExchangeProvider(ExchangeProvider provider) { + selectedProviders.add(provider); + } + + @action + void removeExchangeProvider(ExchangeProvider provider) { + selectedProviders.remove(provider); + } + + @action + void saveSelectedProviders() { + depositAmount = ''; + receiveAmount = ''; + isFixedRateMode = false; + _defineIsReceiveAmountEditable(); + loadLimits(); + + final Map exchangeProvidersSelection = json + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + + exchangeProvidersSelection.updateAll((key, dynamic value) => false); + for (var provider in selectedProviders) { + exchangeProvidersSelection[provider.title] = true; + } + + sharedPreferences.setString( + PreferencesKey.exchangeProvidersSelection, + json.encode(exchangeProvidersSelection), + ); + } + + bool get isAvailableInSelected { + final providersForPair = providersForCurrentPair(); + return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element)); } } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d82b55911..4d4a6a1dd 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Handel für ${title} wird nicht erstellt.", - "trade_not_created" : "Handel nicht erstellt.", + "trade_not_created" : "Handel nicht erstellt", "trade_id_not_found" : "Handel ${tradeId} von ${title} nicht gefunden.", "trade_not_found" : "Handel nicht gefunden.", @@ -586,7 +586,7 @@ "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.", "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.", "optionally_order_card": "Optioneel een fysieke kaart bestellen.", - "hide_details" : "Details verbergen", + "hide_details" : "Details verbergen", "show_details" : "Toon details", "upto": "tot ${value}", "discount": "Bespaar ${value}%", @@ -634,5 +634,10 @@ "contact_support": "Support kontaktieren", "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden", "introducing_cake_pay": "Einführung von Cake Pay!", - "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!" + "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!", + "automatic": "Automatisch", + "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", + "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", + "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen", + "choose_one": "Wähle ein" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 077e81772..958e777f5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Trade for ${title} is not created.", - "trade_not_created" : "Trade not created.", + "trade_not_created" : "Trade not created", "trade_id_not_found" : "Trade ${tradeId} of ${title} not found.", "trade_not_found" : "Trade not found.", @@ -634,5 +634,10 @@ "contact_support": "Contact Support", "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time", "introducing_cake_pay": "Introducing Cake Pay!", - "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!" + "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!", + "automatic": "Automatic", + "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", + "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", + "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange", + "choose_one": "Choose one" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 79f70a7b9..cd1177fa7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Comercio por ${title} no se crea.", - "trade_not_created" : "Comercio no se crea.", + "trade_not_created" : "Comercio no se crea", "trade_id_not_found" : "Comercio ${tradeId} de ${title} no encontrado.", "trade_not_found" : "Comercio no encontrado.", @@ -634,5 +634,10 @@ "contact_support": "Contactar con Soporte", "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento", "introducing_cake_pay": "¡Presentamos Cake Pay!", - "cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!" + "cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!", + "automatic": "Automático", + "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", + "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", + "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio", + "choose_one": "Elige uno" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 545583786..ec6782cac 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -364,7 +364,7 @@ "trade_for_not_created" : "L'échange pour ${title} n'est pas créé.", - "trade_not_created" : "Échange non créé.", + "trade_not_created" : "Échange non créé", "trade_id_not_found" : "Échange ${tradeId} de ${title} introuvable.", "trade_not_found" : "Échange introuvable.", @@ -632,5 +632,10 @@ "contact_support": "Contacter l'assistance", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", "introducing_cake_pay": "Présentation de Cake Pay!", - "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !" + "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !", + "automatic": "Automatique", + "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", + "variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés", + "none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange", + "choose_one": "Choisissez-en un" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4d56e3430..50fd5a2d2 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "के लिए व्यापार ${title} निर्मित नहीं है.", - "trade_not_created" : "व्यापार नहीं बनाया गया.", + "trade_not_created" : "व्यापार नहीं बनाया गया", "trade_id_not_found" : "व्यापार ${tradeId} of ${title} नहीं मिला.", "trade_not_found" : "व्यापार नहीं मिला", @@ -634,5 +634,10 @@ "contact_support": "सहायता से संपर्क करें", "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं", "introducing_cake_pay": "परिचय Cake Pay!", - "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!" + "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!", + "automatic": "स्वचालित", + "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", + "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", + "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता", + "choose_one": "एक का चयन" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index db2531d51..3dbbc03be 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Razmjena za ${title} nije izrađena.", - "trade_not_created" : "Razmjena nije izrađena.", + "trade_not_created" : "Razmjena nije izrađena", "trade_id_not_found" : "Razmjena ${tradeId} za ${title} nije pronađena.", "trade_not_found" : "Razmjena nije pronađena.", @@ -634,5 +634,10 @@ "contact_support": "Kontaktirajte podršku", "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", "introducing_cake_pay": "Predstavljamo Cake Pay!", - "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!" + "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!", + "automatic": "Automatski", + "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", + "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", + "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu", + "choose_one": "Izaberi jedan" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index db0459295..2fbdbe66c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Lo scambio per ${title} non è stato creato.", - "trade_not_created" : "Scambio non creato.", + "trade_not_created" : "Scambio non creato", "trade_id_not_found" : "Scambio ${tradeId} di ${title} not trovato.", "trade_not_found" : "Scambio non trovato.", @@ -634,5 +634,10 @@ "contact_support": "Contatta l'assistenza", "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", "introducing_cake_pay": "Presentazione di Cake Pay!", - "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!" + "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!", + "automatic": "Automatico", + "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", + "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", + "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio", + "choose_one": "Scegline uno" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index c0f70cea7..79102b251 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -612,21 +612,21 @@ "cards": "カード", "active": "アクティブ", "redeemed": "償還", - "gift_card_balance_note": "残高が残っているギフトカードがここに表示されます", - "gift_card_redeemed_note": "利用したギフトカードがここに表示されます", - "logout": "ログアウト", - "add_tip": "ヒントを追加", - "percentageOf": "of ${amount}", - "is_percentage": "is", - "search_category": "検索カテゴリ", - "mark_as_redeemed": "償還済みとしてマーク", - "more_options": "その他のオプション", - "awaiting_payment_confirmation": "支払い確認を待っています", - "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。", - "agree": "同意する", - "in_store": "インストア", - "generated_gift_card": "ギフトカードの生成", - "payment_was_received": "お支払いを受け取りました。", + "gift_card_balance_note": "残高が残っているギフトカードがここに表示されます", + "gift_card_redeemed_note": "利用したギフトカードがここに表示されます", + "logout": "ログアウト", + "add_tip": "ヒントを追加", + "percentageOf": "of ${amount}", + "is_percentage": "is", + "search_category": "検索カテゴリ", + "mark_as_redeemed": "償還済みとしてマーク", + "more_options": "その他のオプション", + "awaiting_payment_confirmation": "支払い確認を待っています", + "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。", + "agree": "同意する", + "in_store": "インストア", + "generated_gift_card": "ギフトカードの生成", + "payment_was_received": "お支払いを受け取りました。", "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", "order_id": "注文ID", "gift_card_is_generated": "ギフトカードが生成されます", @@ -634,5 +634,10 @@ "contact_support": "サポートに連絡する", "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。", "introducing_cake_pay": "序章Cake Pay!", - "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。" + "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。", + "automatic": "自動", + "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", + "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", + "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません", + "choose_one": "1 つ選択してください" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index da94dc1bb..42c2202c4 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "거래 ${title} 생성되지 않습니다.", - "trade_not_created" : "거래가 생성되지 않았습니다.", + "trade_not_created" : "거래가 생성되지 않았습니다", "trade_id_not_found" : "무역 ${tradeId} 의 ${title} 찾을 수 없습니다.", "trade_not_found" : "거래를 찾을 수 없습니다.", @@ -634,5 +634,10 @@ "contact_support": "지원팀에 문의", "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", "introducing_cake_pay": "소개 Cake Pay!", - "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!" + "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!", + "automatic": "자동적 인", + "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", + "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", + "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.", + "choose_one": "하나 선택" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 75a7475e5..72b69dfab 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Ruilen voor ${title} is niet gemaakt.", - "trade_not_created" : "Handel niet gecreëerd.", + "trade_not_created" : "Handel niet gecreëerd", "trade_id_not_found" : "Handel ${tradeId} van ${title} niet gevonden.", "trade_not_found" : "Handel niet gevonden.", @@ -533,7 +533,7 @@ "search_language": "Zoektaal", "search_currency": "Zoek valuta", "new_template" : "Nieuwe sjabloon", - "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", + "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "wallet_name_exists": "Portemonnee met die naam bestaat al", "market_place": "Marktplaats", "cake_pay_title": "Cake Pay-cadeaubonnen", @@ -634,5 +634,10 @@ "contact_support": "Contact opnemen met ondersteuning", "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin", "introducing_cake_pay": "Introductie van Cake Pay!", - "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!" + "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!", + "automatic": "automatisch", + "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", + "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", + "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken", + "choose_one": "Kies er een" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index d6c626913..9c6fbce26 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Zamienić się za ${title} nie jest tworzony.", - "trade_not_created" : "Handel nie utworzony.", + "trade_not_created" : "Handel nie utworzony", "trade_id_not_found" : "Handel ${tradeId} of ${title} nie znaleziono.", "trade_not_found" : "Nie znaleziono handlu.", @@ -634,5 +634,10 @@ "contact_support": "Skontaktuj się z pomocą techniczną", "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin", "introducing_cake_pay": "Przedstawiamy Ciasto Pay!", - "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!" + "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!", + "automatic": "Automatyczny", + "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", + "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", + "none_of_selected_providers_can_exchange": "Żaden z wybranych dostawców nie może dokonać tej wymiany", + "choose_one": "Wybierz jeden" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index cdc8e86c0..1f61e5a20 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "A troca por ${title} não foi criada.", - "trade_not_created" : "Troca não criada.", + "trade_not_created" : "Troca não criada", "trade_id_not_found" : "A troca ${tradeId} de ${title} não foi encontrada.", "trade_not_found" : "Troca não encontrada.", @@ -634,5 +634,10 @@ "contact_support": "Contatar Suporte", "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento", "introducing_cake_pay": "Apresentando o Cake Pay!", - "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!" + "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!", + "automatic": "Automático", + "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", + "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", + "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca", + "choose_one": "Escolha um" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 44dad0323..46ea635f8 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "Сделка для ${title} не создана.", - "trade_not_created" : "Сделка не создана.", + "trade_not_created" : "Сделка не создана", "trade_id_not_found" : "Сделка ${tradeId} ${title} не найдена.", "trade_not_found" : "Trade not found.", @@ -634,5 +634,10 @@ "contact_support": "Связаться со службой поддержки", "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.", "introducing_cake_pay": "Представляем Cake Pay!", - "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!" + "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!", + "automatic": "автоматический", + "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", + "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", + "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен", + "choose_one": "Выбери один" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index cd5d574a9..16b38de88 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -365,7 +365,7 @@ "trade_for_not_created" : "Операція для ${title} не створена.", - "trade_not_created" : "Операція не створена.", + "trade_not_created" : "Операція не створена", "trade_id_not_found" : "Операція ${tradeId} ${title} не знайдена.", "trade_not_found" : "Операція не знайдена.", @@ -633,5 +633,10 @@ "contact_support": "Звернутися до служби підтримки", "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin", "introducing_cake_pay": "Представляємо Cake Pay!", - "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!" + "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!", + "automatic": "Автоматичний", + "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", + "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", + "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін", + "choose_one": "Вибери один" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index f7375bdfb..170646ec9 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -366,7 +366,7 @@ "trade_for_not_created" : "交易 ${title} 未创建.", - "trade_not_created" : "未建立交易.", + "trade_not_created" : "未建立交易", "trade_id_not_found" : "交易方式 ${tradeId} 的 ${title} 未找到.", "trade_not_found" : "找不到交易.", @@ -632,5 +632,10 @@ "contact_support": "联系支持", "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡", "introducing_cake_pay": "介绍 Cake Pay!", - "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!" + "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!", + "automatic": "自动的", + "fixed_pair_not_supported": "所选交易所不支持此固定货币对", + "variable_pair_not_supported": "所选交易所不支持此变量对", + "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换", + "choose_one": "选一个" } From a745319ffa6e2559614cc0c4003b04c7f4282ab0 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Thu, 1 Sep 2022 19:14:01 +0300 Subject: [PATCH 44/87] Fix ionia loading states (#493) --- lib/ionia/ionia_create_state.dart | 8 ++++++++ .../ionia/cards/ionia_account_cards_page.dart | 15 +++++++++++++-- .../ionia/cards/ionia_manage_cards_page.dart | 16 ++++++++++++++-- .../ionia/ionia_account_view_model.dart | 7 ++++++- .../ionia/ionia_gift_cards_list_view_model.dart | 6 ++++++ 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart index b0277be45..3c7dd17f2 100644 --- a/lib/ionia/ionia_create_state.dart +++ b/lib/ionia/ionia_create_state.dart @@ -56,3 +56,11 @@ class IoniaCardSuccess extends IoniaFetchCardState { final IoniaVirtualCard card; } + +abstract class IoniaMerchantState {} + +class IoniaLoadingMerchantState extends IoniaMerchantState {} + +class IoniaLoadedMerchantState extends IoniaMerchantState {} + + diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart index f1d08a75e..bcce3bf13 100644 --- a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart @@ -1,7 +1,6 @@ -import 'dart:ffi'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; @@ -106,6 +105,7 @@ class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProvide _IoniaCardListView( emptyText: S.of(context).gift_card_balance_note, merchList: viewModel.activeMechs, + isLoading: viewModel.merchantState is IoniaLoadingMerchantState, onTap: (giftCard) { Navigator.pushNamed( context, @@ -116,6 +116,7 @@ class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProvide _IoniaCardListView( emptyText: S.of(context).gift_card_redeemed_note, merchList: viewModel.redeemedMerchs, + isLoading: viewModel.merchantState is IoniaLoadingMerchantState, onTap: (giftCard) { Navigator.pushNamed( context, @@ -139,14 +140,24 @@ class _IoniaCardListView extends StatelessWidget { @required this.emptyText, @required this.merchList, @required this.onTap, + this.isLoading = false, }) : super(key: key); final String emptyText; final List merchList; final void Function(IoniaGiftCard giftCard) onTap; + final bool isLoading; @override Widget build(BuildContext context) { + if(isLoading){ + return Center( + child: CircularProgressIndicator( + backgroundColor: Theme.of(context).accentTextTheme.display3.backgroundColor, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryTextTheme.body1.color), + ), + ); + } return merchList.isEmpty ? Center( child: Text( diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart index 7e4f02bf8..f8b28e69e 100644 --- a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -208,7 +209,10 @@ class _IoniaManageCardsPageBodyState extends State { @override Widget build(BuildContext context) { return Observer( - builder: (_) => Stack(children: [ + builder: (_) { + final merchantState = widget.cardsListViewModel.merchantState; + if (merchantState is IoniaLoadedMerchantState) { + return Stack(children: [ ListView.separated( padding: EdgeInsets.only(left: 2, right: 22), controller: _scrollController, @@ -241,7 +245,15 @@ class _IoniaManageCardsPageBodyState extends State { fromTop: widget.cardsListViewModel.scrollOffsetFromTop, ) : Offstage() - ]), + ]); + } + return Center( + child: CircularProgressIndicator( + backgroundColor: Theme.of(context).accentTextTheme.display3.backgroundColor, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryTextTheme.body1.color), + ), + ); + } ); } } diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart index a4875ec61..5801af1dd 100644 --- a/lib/view_model/ionia/ionia_account_view_model.dart +++ b/lib/view_model/ionia/ionia_account_view_model.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; @@ -23,6 +23,9 @@ abstract class IoniaAccountViewModelBase with Store { @observable List giftCards; + @observable + IoniaMerchantState merchantState; + @computed int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length; @@ -39,6 +42,8 @@ abstract class IoniaAccountViewModelBase with Store { @action Future updateUserGiftCards() async { + merchantState = IoniaLoadingMerchantState(); giftCards = await ioniaService.getCurrentUserGiftCardSummaries(); + merchantState = IoniaLoadedMerchantState(); } } diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart index 583a4d5ab..a04fce3e4 100644 --- a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart @@ -39,6 +39,9 @@ abstract class IoniaGiftCardsListViewModelBase with Store { @observable IoniaFetchCardState cardState; + @observable + IoniaMerchantState merchantState; + @observable List ioniaMerchants; @@ -86,10 +89,13 @@ abstract class IoniaGiftCardsListViewModelBase with Store { } void _getMerchants() { + merchantState = IoniaLoadingMerchantState(); ioniaService.getMerchantsByFilter(categories: selectedFilters).then((value) { value.sort((a, b) => a.legalName.toLowerCase().compareTo(b.legalName.toLowerCase())); ioniaMerchants = ioniaMerchantList = value; + merchantState = IoniaLoadedMerchantState(); }); + } @action From 0931696fa10f8fe706e8743844d114f68b95b343 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Thu, 1 Sep 2022 19:29:58 +0300 Subject: [PATCH 45/87] CW-143 cake pay custom tip amount (#492) * Add custom tip logic * Fix euro currency --- lib/di.dart | 17 +++++++--- lib/ionia/ionia_tip.dart | 3 +- .../cards/ionia_buy_card_detail_page.dart | 25 ++++++++++++--- .../ionia/cards/ionia_custom_tip_page.dart | 18 +++++------ .../ionia/ionia_custom_tip_view_model.dart | 31 +++++++++++++++++++ .../ionia_purchase_merch_view_model.dart | 1 + 6 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 lib/view_model/ionia/ionia_custom_tip_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index 081aec364..94045e913 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:cake_wallet/ionia/ionia_api.dart'; @@ -731,16 +733,21 @@ Future setup( ioniaService: getIt.get(), giftCard: giftCard); }); - + + getIt.registerFactoryParam((List args, _) { + final amount = args[0] as double; + final merchant = args[1] as IoniaMerchant; + final tip = args[2] as IoniaTip; + + return IoniaCustomTipViewModel(amount: amount, tip: tip, ioniaMerchant: merchant); + }); + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); }); getIt.registerFactoryParam((List args, _) { - final amount = args.first as String; - final merchant = args.last as IoniaMerchant; - - return IoniaCustomTipPage(getIt.get(param1: amount, param2: merchant)); + return IoniaCustomTipPage(getIt.get(param1: args)); }); getIt.registerFactory(() => IoniaManageCardsPage(getIt.get())); diff --git a/lib/ionia/ionia_tip.dart b/lib/ionia/ionia_tip.dart index 340c6226f..a4425396c 100644 --- a/lib/ionia/ionia_tip.dart +++ b/lib/ionia/ionia_tip.dart @@ -1,7 +1,8 @@ class IoniaTip { - const IoniaTip({this.originalAmount, this.percentage}); + const IoniaTip({this.originalAmount, this.percentage, this.isCustom = false}); final double originalAmount; final double percentage; + final bool isCustom; double get additionalAmount => double.parse((originalAmount * percentage / 100).toStringAsFixed(2)); static const tipList = [ diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart index 6eeb8c93d..5b0109d0e 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -156,7 +156,7 @@ class IoniaBuyGiftCardDetailPage extends BasePage { ), ), Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.fromLTRB(24.0, 24.0, 0, 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -174,6 +174,8 @@ class IoniaBuyGiftCardDetailPage extends BasePage { selectedTip: ioniaPurchaseViewModel.selectedTip.percentage, tipsList: ioniaPurchaseViewModel.tips, onSelect: (value) => ioniaPurchaseViewModel.addTip(value), + amount: ioniaPurchaseViewModel.amount, + merchant: ioniaPurchaseViewModel.ioniaMerchant, ), ) ], @@ -372,13 +374,19 @@ class TipButtonGroup extends StatelessWidget { @required this.selectedTip, @required this.onSelect, @required this.tipsList, + @required this.amount, + @required this.merchant, }) : super(key: key); final Function(IoniaTip) onSelect; final double selectedTip; final List tipsList; + final double amount; + final IoniaMerchant merchant; bool _isSelected(double value) => selectedTip == value; + Set get filter => tipsList.map((e) => e.percentage).toSet(); + bool get _isCustomSelected => !filter.contains(selectedTip); @override Widget build(BuildContext context) { @@ -392,10 +400,17 @@ class TipButtonGroup extends StatelessWidget { return Padding( padding: EdgeInsets.only(right: 5), child: TipButton( - isSelected: _isSelected(tip.percentage), - onTap: () => onSelect(tip), - caption: '${tip.percentage.toStringAsFixed(0)}%', - subTitle: '\$${tip.additionalAmount.toStringAsFixed(2)}', + isSelected: tip.isCustom ? _isCustomSelected : _isSelected(tip.percentage), + onTap: () async { + IoniaTip ioniaTip = tip; + if(tip.isCustom){ + final customTip = await Navigator.pushNamed(context, Routes.ioniaCustomTipPage, arguments: [amount, merchant, tip]) as IoniaTip; + ioniaTip = customTip ?? tip; + } + onSelect(ioniaTip); + }, + caption: tip.isCustom ? S.of(context).custom : '${tip.percentage.toStringAsFixed(0)}%', + subTitle: tip.isCustom ? null : '\$${tip.additionalAmount.toStringAsFixed(2)}', )); })); } diff --git a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart index 5dbce02b6..506f1e736 100644 --- a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart +++ b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart @@ -6,7 +6,7 @@ import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -15,15 +15,15 @@ import 'package:cake_wallet/generated/i18n.dart'; class IoniaCustomTipPage extends BasePage { IoniaCustomTipPage( - this.ioniaPurchaseViewModel, + this.customTipViewModel, ) : _amountFieldFocus = FocusNode(), _amountController = TextEditingController() { _amountController.addListener(() { - // ioniaPurchaseViewModel.onTipChanged(_amountController.text); + customTipViewModel.onTipChanged(_amountController.text); }); } - final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel; + final IoniaCustomTipViewModel customTipViewModel; @override @@ -46,7 +46,7 @@ class IoniaCustomTipPage extends BasePage { @override Widget body(BuildContext context) { final _width = MediaQuery.of(context).size.width; - final merchant = ioniaPurchaseViewModel.ioniaMerchant; + final merchant = customTipViewModel.ioniaMerchant; return KeyboardActions( disableScroll: true, config: KeyboardActionsConfig( @@ -116,7 +116,7 @@ class IoniaCustomTipPage extends BasePage { ), SizedBox(height: 8), Observer(builder: (_) { - if (ioniaPurchaseViewModel.percentage == 0.0) { + if (customTipViewModel.percentage == 0.0) { return SizedBox.shrink(); } @@ -129,8 +129,8 @@ class IoniaCustomTipPage extends BasePage { ), children: [ TextSpan(text: ' ${S.of(context).is_percentage} '), - TextSpan(text: '${ioniaPurchaseViewModel.percentage}%'), - TextSpan(text: ' ${S.of(context).percentageOf(ioniaPurchaseViewModel.amount.toString())} '), + TextSpan(text: '${customTipViewModel.percentage.toStringAsFixed(2)}%'), + TextSpan(text: ' ${S.of(context).percentageOf(customTipViewModel.amount.toStringAsFixed(2))} '), ], ), ); @@ -159,7 +159,7 @@ class IoniaCustomTipPage extends BasePage { padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( onPressed: () { - Navigator.of(context).pop(_amountController.text); + Navigator.of(context).pop(customTipViewModel.customTip); }, text: S.of(context).add_tip, color: Theme.of(context).accentTextTheme.body2.color, diff --git a/lib/view_model/ionia/ionia_custom_tip_view_model.dart b/lib/view_model/ionia/ionia_custom_tip_view_model.dart new file mode 100644 index 000000000..8debc7475 --- /dev/null +++ b/lib/view_model/ionia/ionia_custom_tip_view_model.dart @@ -0,0 +1,31 @@ +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_custom_tip_view_model.g.dart'; + +class IoniaCustomTipViewModel = IoniaCustomTipViewModelBase with _$IoniaCustomTipViewModel; + +abstract class IoniaCustomTipViewModelBase with Store { + IoniaCustomTipViewModelBase({this.amount, this.tip, this.ioniaMerchant}){ + customTip = tip; + percentage = 0; + } + final IoniaMerchant ioniaMerchant; + final double amount; + final IoniaTip tip; + + @observable + IoniaTip customTip; + + @observable + double percentage; + + @action + void onTipChanged(String value){ + + final _amount = value.isEmpty ? 0 : double.parse(value.replaceAll(',', '.')); + percentage = _amount/amount * 100; + customTip = IoniaTip(percentage: percentage, originalAmount: amount); + } +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart index b8c8eaa96..e754ee5ec 100644 --- a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart +++ b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart @@ -25,6 +25,7 @@ abstract class IoniaMerchPurchaseViewModelBase with Store { IoniaTip(percentage: 15, originalAmount: amount), IoniaTip(percentage: 18, originalAmount: amount), IoniaTip(percentage: 20, originalAmount: amount), + IoniaTip(percentage: 0, originalAmount: amount, isCustom: true), ]; selectedTip = tips.first; } From 04be884357bd8a2e71a415a082295fbbff2ef20b Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Thu, 1 Sep 2022 19:46:14 +0100 Subject: [PATCH 46/87] Enable exchange for haven wallets. (#495) --- .../changenow/changenow_exchange_provider.dart | 2 ++ .../sideshift/sideshift_exchange_provider.dart | 2 ++ lib/src/screens/dashboard/dashboard_page.dart | 18 +----------------- .../dashboard/dashboard_view_model.dart | 2 +- .../exchange/exchange_view_model.dart | 13 ++++++++----- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index df5afe5ab..e02ae66a9 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -19,7 +19,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { : _lastUsedRateId = '', super( pairList: CryptoCurrency.all + .where((i) => i != CryptoCurrency.xhv) .map((i) => CryptoCurrency.all + .where((i) => i != CryptoCurrency.xhv) .map((k) => ExchangePair(from: i, to: k, reverse: true)) .where((c) => c != null)) .expand((i) => i) diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index 878ffd528..7b2531873 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -19,7 +19,9 @@ class SideShiftExchangeProvider extends ExchangeProvider { SideShiftExchangeProvider() : super( pairList: CryptoCurrency.all + .where((i) => i != CryptoCurrency.xhv) .map((i) => CryptoCurrency.all + .where((i) => i != CryptoCurrency.xhv) .map((k) => ExchangePair(from: i, to: k, reverse: true)) .where((c) => c != null)) .expand((i) => i) diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 6ed07850f..607e5c3f9 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -314,22 +314,6 @@ class DashboardPage extends BasePage { } Future _onClickExchangeButton(BuildContext context) async { - final walletType = walletViewModel.type; - - switch (walletType) { - case WalletType.haven: - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: 'Exchange', - alertContent: 'Exchange for this asset is not supported yet.', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - break; - default: - await Navigator.of(context).pushNamed(Routes.exchange); - } + await Navigator.of(context).pushNamed(Routes.exchange); } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 1a7a44406..b5d50e7e4 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -337,7 +337,7 @@ abstract class DashboardViewModelBase with Store { } void updateActions() { - isEnabledExchangeAction = wallet.type != WalletType.haven; + isEnabledExchangeAction = true; hasExchangeAction = !isHaven; isEnabledBuyAction = wallet.type != WalletType.haven && wallet.type != WalletType.monero; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d91c393ab..d55d4f9df 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -40,10 +40,10 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, this.tradesStore, this._settingsStore, this.sharedPreferences) { - const excludeDepositCurrencies = [CryptoCurrency.xhv]; - const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv]; + const excludeDepositCurrencies = []; + const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb]; providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()]; - + _initialPairBasedOnWallet(); currentTradeAvailableProviders = SplayTreeMap(); final Map exchangeProvidersSelection = json @@ -51,13 +51,12 @@ abstract class ExchangeViewModelBase with Store { /// if the provider is not in the user settings (user's first time or newly added provider) /// then use its default value decided by us - selectedProviders = ObservableList.of(providerList.where( + selectedProviders = ObservableList.of(providersForCurrentPair().where( (element) => exchangeProvidersSelection[element.title] == null ? element.isEnabled : (exchangeProvidersSelection[element.title] as bool)) .toList()); - _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); depositAmount = ''; @@ -532,6 +531,10 @@ abstract class ExchangeViewModelBase with Store { depositCurrency = CryptoCurrency.ltc; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.haven: + depositCurrency = CryptoCurrency.xhv; + receiveCurrency = CryptoCurrency.btc; + break; default: break; } From 0aee6e1b8dcf9926a1b455bf5eebb6256900e277 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 2 Sep 2022 16:10:54 +0300 Subject: [PATCH 47/87] rework trade details screen (#449) * create standart list card item * create standart list status item * update localization * fix date format * fix theme gradient * PR comments * fix issues from code review --- lib/di.dart | 3 +- .../widgets/sync_indicator_icon.dart | 54 ++++++++++--- .../trade_details_list_card.dart | 35 +++++++++ .../trade_details/trade_details_page.dart | 23 +++++- .../trade_details_status_item.dart | 7 ++ lib/src/widgets/standard_list.dart | 6 ++ lib/src/widgets/standart_list_card.dart | 77 +++++++++++++++++++ lib/src/widgets/standart_list_status_row.dart | 66 ++++++++++++++++ lib/utils/date_formatter.dart | 16 ++-- lib/view_model/trade_details_view_model.dart | 30 ++++++-- res/values/strings_en.arb | 2 +- res/values/strings_fr.arb | 2 +- res/values/strings_hi.arb | 2 +- res/values/strings_hr.arb | 2 +- res/values/strings_nl.arb | 2 +- 15 files changed, 296 insertions(+), 31 deletions(-) create mode 100644 lib/src/screens/trade_details/trade_details_list_card.dart create mode 100644 lib/src/screens/trade_details/trade_details_status_item.dart create mode 100644 lib/src/widgets/standart_list_card.dart create mode 100644 lib/src/widgets/standart_list_status_row.dart diff --git a/lib/di.dart b/lib/di.dart index 94045e913..b43e8a86a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -574,7 +574,8 @@ Future setup( (WalletType type, _) => PreSeedPage(type)); getIt.registerFactoryParam((trade, _) => - TradeDetailsViewModel(tradeForDetails: trade, trades: _tradesSource)); + TradeDetailsViewModel(tradeForDetails: trade, trades: _tradesSource, + settingsStore: getIt.get())); getIt.registerFactory(() => BackupService( getIt.get(), diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart index a9baed3c6..c11920bb7 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart @@ -2,20 +2,56 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; class SyncIndicatorIcon extends StatelessWidget { - SyncIndicatorIcon({this.isSynced}); + SyncIndicatorIcon( + {this.boolMode = true, + this.isSynced = false, + this.value = waiting, + this.size = 4.0}); + final bool boolMode; final bool isSynced; + final String value; + final double size; + + static const String waiting = 'waiting'; + static const String actionRequired = 'action required'; + static const String created = 'created'; + static const String fetching = 'fetching'; + static const String finished = 'finished'; @override Widget build(BuildContext context) { + Color indicatorColor; + + if (boolMode) { + indicatorColor = isSynced + ? PaletteDark.brightGreen + : Theme.of(context).textTheme.caption.color; + } else { + switch (value.toLowerCase()) { + case waiting: + indicatorColor = Colors.red; + break; + case actionRequired: + indicatorColor = Theme.of(context).textTheme.display3.decorationColor; + break; + case created: + indicatorColor = PaletteDark.brightGreen; + break; + case fetching: + indicatorColor = Colors.red; + break; + case finished: + indicatorColor = PaletteDark.brightGreen; + break; + default: + indicatorColor = Colors.red; + } + } return Container( - height: 4, - width: 4, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isSynced - ? PaletteDark.brightGreen - : Theme.of(context).textTheme.caption.color), - ); + height: size, + width: size, + decoration: + BoxDecoration(shape: BoxShape.circle, color: indicatorColor)); } } diff --git a/lib/src/screens/trade_details/trade_details_list_card.dart b/lib/src/screens/trade_details/trade_details_list_card.dart new file mode 100644 index 000000000..9df8f1011 --- /dev/null +++ b/lib/src/screens/trade_details/trade_details_list_card.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class TradeDetailsListCardItem extends StandartListItem { + TradeDetailsListCardItem( + {String title, + String value, + this.id, + this.createdAt, + this.pair, + this.onTap}) + : super(title: title, value: value); + + factory TradeDetailsListCardItem.tradeDetails( + {@required String id, + @required String createdAt, + @required CryptoCurrency from, + @required CryptoCurrency to, + @required Function onTap}) { + return TradeDetailsListCardItem( + id: '${S.current.trade_details_id} ${formatAsText(id)}', + createdAt: formatAsText(createdAt), + pair: '${formatAsText(from)} → ${formatAsText(to)}', + onTap: onTap); + } + + final String id; + final String createdAt; + final String pair; + final Function onTap; + + static String formatAsText(T value) => value?.toString() ?? ''; +} diff --git a/lib/src/screens/trade_details/trade_details_page.dart b/lib/src/screens/trade_details/trade_details_page.dart index abf8e2873..b46db87fa 100644 --- a/lib/src/screens/trade_details/trade_details_page.dart +++ b/lib/src/screens/trade_details/trade_details_page.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/src/widgets/standart_list_card.dart'; +import 'package:cake_wallet/src/widgets/standart_list_status_row.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; import 'package:flutter/material.dart'; @@ -9,6 +11,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/standart_list_row.dart'; import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; class TradeDetailsPage extends BasePage { TradeDetailsPage(this.tradeDetailsViewModel); @@ -58,7 +62,23 @@ class TradeDetailsPageBodyState extends State { onTap: item.onTap, child: StandartListRow( title: '${item.title}', value: '${item.value}')); - } else { + } + + if (item is DetailsListStatusItem) { + return StandartListStatusRow( + title: item.title, + value: item.value); + } + + if (item is TradeDetailsListCardItem) { + return TradeDatailsStandartListCard( + id: item.id, + create: item.createdAt, + pair: item.pair, + currentTheme: tradeDetailsViewModel.settingsStore.currentTheme.type, + onTap: item.onTap,); + } + return GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: '${item.value}')); @@ -66,7 +86,6 @@ class TradeDetailsPageBodyState extends State { }, child: StandartListRow( title: '${item.title}', value: '${item.value}')); - } }); }); } diff --git a/lib/src/screens/trade_details/trade_details_status_item.dart b/lib/src/screens/trade_details/trade_details_status_item.dart new file mode 100644 index 000000000..eea7ccdf4 --- /dev/null +++ b/lib/src/screens/trade_details/trade_details_status_item.dart @@ -0,0 +1,7 @@ +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; + +class DetailsListStatusItem extends StandartListItem { + DetailsListStatusItem( + {String title, String value}) + : super(title: title, value: value); +} diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index 572f03791..26bee8ada 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/widgets/standart_list_card.dart'; +import 'package:cake_wallet/src/widgets/standart_list_status_row.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -212,6 +214,10 @@ class SectionStandardList extends StatelessWidget { return Container(); } + if (row is StandartListStatusRow || row is TradeDatailsStandartListCard) { + return Container(); + } + final nextRow = totalRows[index + 1]; // If current row is pre last and last row is separator. diff --git a/lib/src/widgets/standart_list_card.dart b/lib/src/widgets/standart_list_card.dart new file mode 100644 index 000000000..f3bc89b99 --- /dev/null +++ b/lib/src/widgets/standart_list_card.dart @@ -0,0 +1,77 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; + +class TradeDatailsStandartListCard extends StatelessWidget { + TradeDatailsStandartListCard( + {this.id, this.create, this.pair, this.onTap, this.currentTheme}); + + final String id; + final String create; + final String pair; + final ThemeType currentTheme; + final Function onTap; + + @override + Widget build(BuildContext context) { + final darkTheme = currentTheme == ThemeType.dark; + + final baseGradient = LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subtitle.color, + Theme.of(context).primaryTextTheme.subtitle.decorationColor, + ], begin: Alignment.centerLeft, end: Alignment.centerRight); + + final gradient = LinearGradient(colors: [ + PaletteDark.wildNightBlue, + PaletteDark.oceanBlue, + ], begin: Alignment.bottomCenter, end: Alignment.topCenter); + + final textColor = Colors.white; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + child: GestureDetector( + onTap: () => onTap(context), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + gradient: darkTheme ? gradient : baseGradient), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(id, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: textColor)), + SizedBox( + height: 8, + ), + Text(create, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: textColor)), + SizedBox( + height: 35, + ), + Text(pair, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + color: textColor)), + ]), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/standart_list_status_row.dart b/lib/src/widgets/standart_list_status_row.dart new file mode 100644 index 000000000..a096dcc8d --- /dev/null +++ b/lib/src/widgets/standart_list_status_row.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class StandartListStatusRow extends StatelessWidget { + StandartListStatusRow({this.title, this.value}); + + final String title; + final String value; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + color: Theme.of(context).backgroundColor, + child: Padding( + padding: + const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.overline.color), + textAlign: TextAlign.left), + Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).accentTextTheme.display2.color, + borderRadius: BorderRadius.circular(30.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SyncIndicatorIcon( + boolMode: false, + value: value, + size: 6, + ), + SizedBox( + width: 5, + ), + Text(value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color)) + ], + ), + ), + ), + ) + ]), + ), + ); + } +} diff --git a/lib/utils/date_formatter.dart b/lib/utils/date_formatter.dart index f75c4d729..9cff68614 100644 --- a/lib/utils/date_formatter.dart +++ b/lib/utils/date_formatter.dart @@ -3,21 +3,21 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; class DateFormatter { - static String currentLocalFormat({bool hasTime = true}) { + static String currentLocalFormat({bool hasTime = true, bool reverse = false}) { final isUSA = getIt.get().languageCode.toLowerCase() == 'en'; final format = - isUSA ? usaStyleFormat(hasTime) : regularStyleFormat(hasTime); + isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); return format; } - static DateFormat withCurrentLocal({bool hasTime = true}) => DateFormat( - currentLocalFormat(hasTime: hasTime), + static DateFormat withCurrentLocal({bool hasTime = true, bool reverse = false}) => DateFormat( + currentLocalFormat(hasTime: hasTime, reverse: reverse), getIt.get().languageCode); - static String usaStyleFormat(bool hasTime) => - hasTime ? 'yyyy.MM.dd, HH:mm' : 'yyyy.MM.dd'; + static String usaStyleFormat(bool hasTime, bool reverse) => + hasTime ? (reverse ? 'HH:mm yyyy.MM.dd' : 'yyyy.MM.dd, HH:mm') : 'yyyy.MM.dd'; - static String regularStyleFormat(bool hasTime) => - hasTime ? 'dd.MM.yyyy, HH:mm' : 'dd.MM.yyyy'; + static String regularStyleFormat(bool hasTime, bool reverse) => + hasTime ? (reverse ? 'HH:mm dd.MM.yyyy' : 'dd.MM.yyyy, HH:mm') : 'dd.MM.yyyy'; } diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 00aabdebd..59b52463b 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -7,12 +7,18 @@ import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart' import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; import 'package:url_launcher/url_launcher.dart'; part 'trade_details_view_model.g.dart'; @@ -20,7 +26,7 @@ class TradeDetailsViewModel = TradeDetailsViewModelBase with _$TradeDetailsViewModel; abstract class TradeDetailsViewModelBase with Store { - TradeDetailsViewModelBase({Trade tradeForDetails, this.trades}) { + TradeDetailsViewModelBase({Trade tradeForDetails, this.trades, this.settingsStore}) { trade = tradeForDetails; switch (trade.provider) { @@ -62,6 +68,8 @@ abstract class TradeDetailsViewModelBase with Store { Timer timer; + final SettingsStore settingsStore; + @action Future _updateTrade() async { try { @@ -80,18 +88,28 @@ abstract class TradeDetailsViewModelBase with Store { } void _updateItems() { - final dateFormat = DateFormatter.withCurrentLocal(); + final dateFormat = DateFormatter.withCurrentLocal(reverse: true); items?.clear(); - items.addAll([ - StandartListItem(title: S.current.trade_details_id, value: trade.id), - StandartListItem( + items.add( + DetailsListStatusItem( title: S.current.trade_details_state, value: trade.state != null ? trade.state.toString() : S.current.trade_details_fetching) - ]); + ); + + items.add(TradeDetailsListCardItem.tradeDetails( + id: trade.id, + createdAt: dateFormat.format(trade.createdAt), + from: trade.from, + to: trade.to, + onTap: (BuildContext context) { + Clipboard.setData(ClipboardData(text: '${trade.id}')); + showBar(context, S.of(context).copied_to_clipboard); + }, + )); if (trade.provider != null) { items.add(StandartListItem( diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 958e777f5..f66eee460 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -276,7 +276,7 @@ "trade_details_title" : "Trade Details", "trade_details_id" : "ID", - "trade_details_state" : "State", + "trade_details_state" : "Status", "trade_details_fetching" : "Fetching", "trade_details_provider" : "Provider", "trade_details_created_at" : "Created at", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ec6782cac..4737d6a0f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -274,7 +274,7 @@ "trade_details_title" : "Détails de l'échange", "trade_details_id" : "ID", - "trade_details_state" : "État", + "trade_details_state" : "Statut", "trade_details_fetching" : "Récupération", "trade_details_provider" : "Fournisseur", "trade_details_created_at" : "Créé le", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 50fd5a2d2..0e5bd72bc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -276,7 +276,7 @@ "trade_details_title" : "व्यापार विवरण", "trade_details_id" : "आईडी", - "trade_details_state" : "राज्य", + "trade_details_state" : "दर्जा", "trade_details_fetching" : "ला रहा है", "trade_details_provider" : "प्रदाता", "trade_details_created_at" : "पर बनाया गया", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 3dbbc03be..6ddd2ef51 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -276,7 +276,7 @@ "trade_details_title" : "Detalji razmjene", "trade_details_id" : "ID", - "trade_details_state" : "Stanje", + "trade_details_state" : "Status", "trade_details_fetching" : "Dohvaćanje", "trade_details_provider" : "Pružatelj", "trade_details_created_at" : "Stvoreno u", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 72b69dfab..416479bab 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -276,7 +276,7 @@ "trade_details_title" : "Handelsgegevens", "trade_details_id" : "ID", - "trade_details_state" : "Staat", + "trade_details_state" : "Toestand", "trade_details_fetching" : "Ophalen", "trade_details_provider" : "Leverancier", "trade_details_created_at" : "Gemaakt bij", From bd25d047b2dc648ddc776b760eec2fd32c02433b Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 2 Sep 2022 16:47:45 +0300 Subject: [PATCH 48/87] CW-148 cake pay partial redemptions (#490) * Ionia custom redemption screen * update ionia gift card remaining amount with custom value * [NO-TASK] Fix issues with custom redeem * replace redeem * update remaining amount * fixed from code review * Add localization --- lib/di.dart | 18 ++ lib/ionia/ionia_gift_card.dart | 3 +- lib/router.dart | 10 ++ lib/routes.dart | 2 + .../ionia/cards/ionia_custom_redeem_page.dart | 167 ++++++++++++++++++ .../cards/ionia_gift_card_detail_page.dart | 54 ++++-- .../ionia/cards/ionia_more_options_page.dart | 90 ++++++++++ lib/src/screens/ionia/widgets/card_item.dart | 3 + lib/src/widgets/discount_badge.dart | 4 +- .../ionia/ionia_custom_redeem_view_model.dart | 27 +++ .../ionia_gift_card_details_view_model.dart | 10 ++ res/values/strings_de.arb | 6 +- res/values/strings_en.arb | 6 +- res/values/strings_es.arb | 6 +- res/values/strings_fr.arb | 6 +- res/values/strings_hi.arb | 6 +- res/values/strings_hr.arb | 6 +- res/values/strings_it.arb | 6 +- res/values/strings_ja.arb | 6 +- res/values/strings_ko.arb | 6 +- res/values/strings_nl.arb | 6 +- res/values/strings_pl.arb | 6 +- res/values/strings_pt.arb | 6 +- res/values/strings_ru.arb | 6 +- res/values/strings_uk.arb | 6 +- res/values/strings_zh.arb | 6 +- 26 files changed, 446 insertions(+), 32 deletions(-) create mode 100644 lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart create mode 100644 lib/src/screens/ionia/cards/ionia_more_options_page.dart create mode 100644 lib/view_model/ionia/ionia_custom_redeem_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index b43e8a86a..02a86d1e5 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -5,10 +5,13 @@ import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:cake_wallet/ionia/ionia_api.dart'; @@ -747,6 +750,21 @@ Future setup( return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); }); + getIt.registerFactoryParam((List args, _){ + final giftCard = args.first as IoniaGiftCard; + + return IoniaMoreOptionsPage(giftCard); + }); + + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) => IoniaCustomRedeemViewModel(giftCard)); + + getIt.registerFactoryParam((List args, _){ + final giftCard = args.first as IoniaGiftCard; + + return IoniaCustomRedeemPage(getIt.get(param1: giftCard) ); + }); + + getIt.registerFactoryParam((List args, _) { return IoniaCustomTipPage(getIt.get(param1: args)); }); diff --git a/lib/ionia/ionia_gift_card.dart b/lib/ionia/ionia_gift_card.dart index ebe8084f8..adbd16f0c 100644 --- a/lib/ionia/ionia_gift_card.dart +++ b/lib/ionia/ionia_gift_card.dart @@ -60,10 +60,11 @@ class IoniaGiftCard { final double actualAmount; final double totalTransactionAmount; final double totalDashTransactionAmount; - final double remainingAmount; + double remainingAmount; final String createdDateFormatted; final String lastTransactionDateFormatted; final bool isActive; final bool isEmpty; final String logoUrl; + } \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index 1a9c3c2d5..051e07cb0 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -6,8 +6,10 @@ import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; @@ -452,6 +454,14 @@ Route createRoute(RouteSettings settings) { case Routes.ioniaGiftCardDetailPage: final args = settings.arguments as List; return CupertinoPageRoute(builder: (_) => getIt.get(param1: args.first)); + + case Routes.ioniaCustomRedeemPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.ioniaMoreOptionsPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.ioniaPaymentStatusPage: final args = settings.arguments as List; diff --git a/lib/routes.dart b/lib/routes.dart index 82bd46691..7431d71df 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -74,4 +74,6 @@ class Routes { static const ioniaCustomTipPage = 'ionia_custom_tip_page'; static const ioniaGiftCardDetailPage = '/ionia_gift_card_detail_page'; static const ioniaPaymentStatusPage = '/ionia_payment_status_page'; + static const ioniaMoreOptionsPage = '/ionia_more_options_page'; + static const ioniaCustomRedeemPage = '/ionia_custom_redeem_page'; } diff --git a/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart b/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart new file mode 100644 index 000000000..4bb76848f --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart @@ -0,0 +1,167 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaCustomRedeemPage extends BasePage { + IoniaCustomRedeemPage( + this.ioniaCustomRedeemViewModel, + ) : _amountFieldFocus = FocusNode(), + _amountController = TextEditingController() { + _amountController.addListener(() { + ioniaCustomRedeemViewModel.updateAmount(_amountController.text); + }); + } + + final IoniaCustomRedeemViewModel ioniaCustomRedeemViewModel; + + + @override + String get title => S.current.custom_redeem_amount; + + @override + Color get titleColor => Colors.white; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); + + final TextEditingController _amountController; + final FocusNode _amountFieldFocus; + + @override + Widget body(BuildContext context) { + final _width = MediaQuery.of(context).size.width; + final giftCard = ioniaCustomRedeemViewModel.giftCard; + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFieldFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 150), + BaseTextFormField( + controller: _amountController, + focusNode: _amountFieldFocus, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))], + hintText: '1000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + fontWeight: FontWeight.w500, + fontSize: 36, + ), + borderColor: Theme.of(context).primaryTextTheme.headline.color, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + fontSize: 36, + ), + suffixIcon: SizedBox( + width: _width / 6, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + top: 5.0, + left: _width / 4, + ), + child: Text( + 'USD: ', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 36, + ), + ), + ), + ), + SizedBox(height: 8), + Observer(builder: (_)=> + !ioniaCustomRedeemViewModel.disableRedeem ? + Center( + child: Text('\$${giftCard.remainingAmount} - \$${ioniaCustomRedeemViewModel.amount} = \$${ioniaCustomRedeemViewModel.remaining} ${S.of(context).remaining}', + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ),), + ) : SizedBox.shrink(), + ), + SizedBox(height: 24), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CardItem( + title: giftCard.legalName, + backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), + discount: giftCard.remainingAmount, + isAmount: true, + discountBackground: AssetImage('assets/images/red_badge_discount.png'), + titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, + subtitleColor: Theme.of(context).hintColor, + subTitle: S.of(context).online, + logoUrl: giftCard.logoUrl, + ), + ), + ], + ), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () { + Navigator.of(context).pop(_amountController.text); + }, + isDisabled: ioniaCustomRedeemViewModel.disableRedeem, + text: S.of(context).add_custom_redemption, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart index dc9746cdf..9132ad52a 100644 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -117,7 +117,7 @@ class IoniaGiftCardDetailPage extends BasePage { buildIoniaTile( context, title: S.of(context).amount, - subTitle: viewModel.giftCard.remainingAmount.toStringAsFixed(2) ?? '0.00', + subTitle: viewModel.remainingAmount.toStringAsFixed(2) ?? '0.00', )), Divider(height: 50), TextIconButton( @@ -127,21 +127,45 @@ class IoniaGiftCardDetailPage extends BasePage { ], ), bottomSection: Padding( - padding: EdgeInsets.only(bottom: 12), - child: Observer(builder: (_) { - if (!viewModel.giftCard.isEmpty) { - return LoadingPrimaryButton( - isLoading: viewModel.redeemState is IsExecutingState, - onPressed: () => viewModel.redeem().then((_){ - Navigator.of(context).pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); - }), - text: S.of(context).mark_as_redeemed, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white); - } + padding: EdgeInsets.only(bottom: 12), + child: Observer( + builder: (_) { + if (!viewModel.giftCard.isEmpty) { + return Column( + children: [ + PrimaryButton( + onPressed: () async { + final amount = await Navigator.of(context) + .pushNamed(Routes.ioniaMoreOptionsPage, arguments: [viewModel.giftCard]) as String; + if (amount != null) { + viewModel.updateRemaining(double.parse(amount)); + } + }, + text: S.of(context).more_options, + color: Theme.of(context).accentTextTheme.caption.color, + textColor: Theme.of(context).primaryTextTheme.title.color, + ), + SizedBox(height: 12), + LoadingPrimaryButton( + isLoading: viewModel.redeemState is IsExecutingState, + onPressed: () => viewModel.redeem().then( + (_) { + Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); + }, + ), + text: S.of(context).mark_as_redeemed, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ], + ); + } - return Container(); - })), + return Container(); + }, + ), + ), ); } diff --git a/lib/src/screens/ionia/cards/ionia_more_options_page.dart b/lib/src/screens/ionia/cards/ionia_more_options_page.dart new file mode 100644 index 000000000..be1a9a702 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_more_options_page.dart @@ -0,0 +1,90 @@ +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; + + +class IoniaMoreOptionsPage extends BasePage { + IoniaMoreOptionsPage(this.giftCard); + + final IoniaGiftCard giftCard; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.more_options, + style: textMediumSemiBold( + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10,), + Center(child: Text(S.of(context).choose_from_available_options, style: textMedium( + color: Theme.of(context).primaryTextTheme.title.color, + ),)), + SizedBox(height: 40,), + InkWell( + onTap: () async { + final amount = await Navigator.of(context).pushNamed(Routes.ioniaCustomRedeemPage, arguments: [giftCard]) as String; + if(amount.isNotEmpty){ + Navigator.pop(context, amount); + } + }, + child: _GradiantContainer( + content: Padding( + padding: const EdgeInsets.only(top: 24, left: 20, right: 24, bottom: 50), + child: Text( + S.of(context).custom_redeem_amount, + style: textXLargeSemiBold(), + ), + ), + ), + ) + ], + ), + ); + } +} + +class _GradiantContainer extends StatelessWidget { + const _GradiantContainer({ + Key key, + @required this.content, + this.padding, + this.width, + }) : super(key: key); + + final Widget content; + final EdgeInsets padding; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + child: content, + width: width, + padding: padding ?? EdgeInsets.all(24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).accentColor, + ], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart index ae3554331..a180c2bdd 100644 --- a/lib/src/screens/ionia/widgets/card_item.dart +++ b/lib/src/screens/ionia/widgets/card_item.dart @@ -12,6 +12,7 @@ class CardItem extends StatelessWidget { this.onTap, this.logoUrl, this.discount, + this.isAmount = false, }); final VoidCallback onTap; @@ -19,6 +20,7 @@ class CardItem extends StatelessWidget { final String subTitle; final String logoUrl; final double discount; + final bool isAmount; final Color backgroundColor; final Color titleColor; final Color subtitleColor; @@ -100,6 +102,7 @@ class CardItem extends StatelessWidget { padding: const EdgeInsets.only(top: 20.0), child: DiscountBadge( percentage: discount, + isAmount: isAmount, discountBackground: discountBackground, ), ), diff --git a/lib/src/widgets/discount_badge.dart b/lib/src/widgets/discount_badge.dart index 1cbbf3e89..4db16bee3 100644 --- a/lib/src/widgets/discount_badge.dart +++ b/lib/src/widgets/discount_badge.dart @@ -4,11 +4,13 @@ import 'package:cake_wallet/generated/i18n.dart'; class DiscountBadge extends StatelessWidget { const DiscountBadge({ Key key, + this.isAmount = false, @required this.percentage, this.discountBackground, }) : super(key: key); final double percentage; + final bool isAmount; final AssetImage discountBackground; @override @@ -16,7 +18,7 @@ class DiscountBadge extends StatelessWidget { return Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( - S.of(context).discount(percentage.toStringAsFixed(2)), + isAmount ? '\$${percentage.toStringAsFixed(2)}' : S.of(context).discount(percentage.toStringAsFixed(2)), style: TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/view_model/ionia/ionia_custom_redeem_view_model.dart b/lib/view_model/ionia/ionia_custom_redeem_view_model.dart new file mode 100644 index 000000000..88cc08e83 --- /dev/null +++ b/lib/view_model/ionia/ionia_custom_redeem_view_model.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:mobx/mobx.dart'; +part 'ionia_custom_redeem_view_model.g.dart'; +class IoniaCustomRedeemViewModel = IoniaCustomRedeemViewModelBase with _$IoniaCustomRedeemViewModel; + +abstract class IoniaCustomRedeemViewModelBase with Store { + IoniaCustomRedeemViewModelBase(this.giftCard){ + amount = 0; + } + + final IoniaGiftCard giftCard; + + @observable + double amount; + + @computed + double get remaining => amount <= giftCard.remainingAmount ? giftCard.remainingAmount - amount : 0; + + @computed + bool get disableRedeem => amount > giftCard.remainingAmount; + + @action + void updateAmount(String text){ + amount = text.isEmpty ? 0 : (double.parse(text.replaceAll(',', '.')) ?? 0); + } + +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart index cbbfc49f1..e118dac43 100644 --- a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -12,6 +12,7 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { IoniaGiftCardDetailsViewModelBase({this.ioniaService, this.giftCard}) { redeemState = InitialExecutionState(); + remainingAmount = giftCard.remainingAmount; } final IoniaService ioniaService; @@ -20,11 +21,15 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { @observable IoniaGiftCard giftCard; + @observable + double remainingAmount; + @observable ExecutionState redeemState; @action Future redeem() async { + giftCard.remainingAmount = remainingAmount; try { redeemState = IsExecutingState(); await ioniaService.redeem(giftCard); @@ -35,6 +40,11 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { } } + @action + void updateRemaining(double amount){ + remainingAmount = amount; + } + void increaseBrightness() async { brightness = await DeviceDisplayBrightness.getBrightness(); await DeviceDisplayBrightness.setBrightness(1.0); diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 4d4a6a1dd..b4a3a923f 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen", - "choose_one": "Wähle ein" + "choose_one": "Wähle ein", + "choose_from_available_options": "Wähle aus verfügbaren Optionen:", + "custom_redeem_amount": "Benutzerdefinierter Einlösungsbetrag", + "add_custom_redemption": "Benutzerdefinierte Einlösung hinzufügen", + "remaining": "Rest" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index f66eee460..c707b0d87 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange", - "choose_one": "Choose one" + "choose_one": "Choose one", + "choose_from_available_options": "Choose from the available options:", + "custom_redeem_amount": "Custom Redeem Amount", + "add_custom_redemption": "Add Custom Redemption", + "remaining": "remaining" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index cd1177fa7..f46fab21c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio", - "choose_one": "Elige uno" + "choose_one": "Elige uno", + "choose_from_available_options": "Elija entre las opciones disponibles:", + "custom_redeem_amount": "Cantidad de canje personalizada", + "add_custom_redemption": "Agregar redención personalizada", + "remaining": "restante" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4737d6a0f..de3f92700 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -637,5 +637,9 @@ "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", "variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés", "none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange", - "choose_one": "Choisissez-en un" + "choose_one": "Choisissez-en un", + "choose_from_available_options": "Choisissez parmi les options disponibles :", + "custom_redeem_amount": "Montant d'échange personnalisé", + "add_custom_redemption": "Ajouter un remboursement personnalisé", + "remaining": "restant" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 0e5bd72bc..96e0dcb6a 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता", - "choose_one": "एक का चयन" + "choose_one": "एक का चयन", + "choose_from_available_options": "उपलब्ध विकल्पों में से चुनें:", + "custom_redeem_amount": "कस्टम रिडीम राशि", + "add_custom_redemption": "कस्टम रिडेम्पशन जोड़ें", + "remaining": "शेष" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6ddd2ef51..00a5f04c0 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu", - "choose_one": "Izaberi jedan" + "choose_one": "Izaberi jedan", + "choose_from_available_options": "Odaberite neku od dostupnih opcija:", + "custom_redeem_amount": "Prilagođeni iznos otkupa", + "add_custom_redemption": "Dodaj prilagođeni otkup", + "remaining": "preostalo" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 2fbdbe66c..3b83ff960 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio", - "choose_one": "Scegline uno" + "choose_one": "Scegline uno", + "choose_from_available_options": "Scegli tra le opzioni disponibili:", + "custom_redeem_amount": "Importo di riscatto personalizzato", + "add_custom_redemption": "Aggiungi riscatto personalizzato", + "remaining": "rimanente" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 79102b251..e909311f6 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません", - "choose_one": "1 つ選択してください" + "choose_one": "1 つ選択してください", + "choose_from_available_options": "利用可能なオプションから選択してください:", + "custom_redeem_amount": "カスタム交換金額", + "add_custom_redemption": "カスタム引き換えを追加", + "remaining": "残り" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 42c2202c4..48db01c9a 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.", - "choose_one": "하나 선택" + "choose_one": "하나 선택", + "choose_from_available_options": "사용 가능한 옵션에서 선택:", + "custom_redeem_amount": "사용자 지정 상환 금액", + "add_custom_redemption": "사용자 지정 상환 추가", + "remaining": "남은" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 416479bab..9e39b271e 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken", - "choose_one": "Kies er een" + "choose_one": "Kies er een", + "choose_from_available_options": "Kies uit de beschikbare opties:", + "custom_redeem_amount": "Aangepast inwisselbedrag", + "add_custom_redemption": "Voeg aangepaste inwisseling toe", + "remaining": "resterende" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9c6fbce26..30eb1eaa3 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", "none_of_selected_providers_can_exchange": "Żaden z wybranych dostawców nie może dokonać tej wymiany", - "choose_one": "Wybierz jeden" + "choose_one": "Wybierz jeden", + "choose_from_available_options": "Wybierz z dostępnych opcji:", + "custom_redeem_amount": "Niestandardowa kwota wykorzystania", + "add_custom_redemption": "Dodaj niestandardowe wykorzystanie", + "remaining": "pozostałe" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 1f61e5a20..eac4ac37c 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca", - "choose_one": "Escolha um" + "choose_one": "Escolha um", + "choose_from_available_options": "Escolha entre as opções disponíveis:", + "custom_redeem_amount": "Valor de resgate personalizado", + "add_custom_redemption": "Adicionar resgate personalizado", + "remaining": "restante" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 46ea635f8..c82d8969a 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -639,5 +639,9 @@ "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен", - "choose_one": "Выбери один" + "choose_one": "Выбери один", + "choose_from_available_options": "Выберите из доступных вариантов:", + "custom_redeem_amount": "Пользовательская сумма погашения", + "add_custom_redemption": "Добавить пользовательское погашение", + "remaining": "осталось" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 16b38de88..caac9111a 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -638,5 +638,9 @@ "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін", - "choose_one": "Вибери один" + "choose_one": "Вибери один", + "choose_from_available_options": "Виберіть із доступних варіантів:", + "custom_redeem_amount": "Власна сума викупу", + "add_custom_redemption": "Додати спеціальне погашення", + "remaining": "залишилося" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 170646ec9..d8b156002 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -637,5 +637,9 @@ "fixed_pair_not_supported": "所选交易所不支持此固定货币对", "variable_pair_not_supported": "所选交易所不支持此变量对", "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换", - "choose_one": "选一个" + "choose_one": "选一个", + "choose_from_available_options": "从可用选项中选择:", + "custom_redeem_amount": "自定义兑换金额", + "add_custom_redemption": "添加自定义兑换", + "remaining": "剩余" } From da46b8bfb489e150e9f83b62b3a11995c514809b Mon Sep 17 00:00:00 2001 From: Paul Verbeke Date: Fri, 2 Sep 2022 16:11:13 +0200 Subject: [PATCH 49/87] i18n: fix french translations (#483) QR code search languages and currencies wallet name errors --- res/values/strings_fr.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index de3f92700..d7bd0c84a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -149,7 +149,7 @@ "subaddresses" : "Sous-adresses", "addresses" : "Adresses", "scan_qr_code" : "Scannez le QR code pour obtenir l'adresse", - "qr_fullscreen" : "Appuyez pour ouvrir le code QR en plein écran", + "qr_fullscreen" : "Appuyez pour ouvrir le QR code en plein écran", "rename" : "Renommer", "choose_account" : "Choisir le compte", "create_new_account" : "Créer un nouveau compte", @@ -325,7 +325,7 @@ "error_text_fiat" : "La valeur du montant ne peut dépasser le solde disponible.\nLa partie décimale doit comporter au plus 2 chiffres", "error_text_subaddress_name" : "Le nom de sous-adresse ne peut contenir les symboles ` , ' \"\net sa longueur doit être comprise entre 1 et 20 caractères", "error_text_amount" : "Le montant ne peut comporter que des nombres", - "error_text_wallet_name" : "Le nom du portefeuille ne peut contenir que des lettres, des chiffres, des symboles _ -\net doit comporter entre 1 et 33 caractères", + "error_text_wallet_name" : "Le nom du portefeuille ne peut contenir que des lettres, des chiffres, des symboles _ -\net sa longueur doit être comprise entre 1 et 33 caractères", "error_text_keys" : "Les clefs du portefeuille ne peuvent être constituées que de 64 caractères hexadécimaux", "error_text_crypto_currency" : "La partie décimale\ndoit comporter au plus 12 chiffres", "error_text_minimal_limit" : "Échange pour ${provider} non créé. Le montant est inférieur au minimum : ${min} ${currency}", @@ -528,11 +528,11 @@ "third_intro_content" : "Les Yats existent aussi en dehors de Cake Wallet. Toute adresse sur terre peut être remplacée par un Yat !", "learn_more" : "En savoir plus", "search": "Chercher", - "search_language": "Langue de recherche", - "search_currency": "Devise de recherche", + "search_language": "Recherche une langue", + "search_currency": "Rechercher une devise", "new_template" : "Nouveau Modèle", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", - "wallet_name_exists": "Le portefeuille portant ce nom existe déjà", + "wallet_name_exists": "Un portefeuille portant ce nom existe déjà", "market_place": "Place de marché", "cake_pay_title": "Cartes cadeaux Cake Pay", "cake_pay_subtitle": "Achetez des cartes-cadeaux et échangez-les instantanément", From 278695005c31cdc78c906589903fb4e07cac3156 Mon Sep 17 00:00:00 2001 From: duggavo <108991918+duggavo@users.noreply.github.com> Date: Fri, 2 Sep 2022 14:13:08 +0000 Subject: [PATCH 50/87] Update strings_it.arb (#433) --- res/values/strings_it.arb | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 3b83ff960..c88254f8c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -7,14 +7,14 @@ "restore_wallet" : "Recupera Portafoglio", "monero_com": "Monero.com by Cake Wallet", - "monero_com_wallet_text": "Awesome wallet for Monero", + "monero_com_wallet_text": "Portafoglio fantastico per Monero", "haven_app": "Haven by Cake Wallet", - "haven_app_wallet_text": "Awesome wallet for Haven", + "haven_app_wallet_text": "Portafoglio fantastico per Haven", - "accounts" : "Accounts", + "accounts" : "Conti", "edit" : "Modifica", - "account" : "Account", + "account" : "Conto", "add" : "Aggiungi", @@ -24,7 +24,7 @@ "cancel" : "Cancella", "ok" : "OK", "contact_name" : "Nome Contatto", - "reset" : "Resetta", + "reset" : "Ripristina", "save" : "Salva", "address_remove_contact" : "Rimuovi contatto", "address_remove_content" : "Sei sicuro di voler eliminare il contatto selezionato?", @@ -35,12 +35,12 @@ "failed_authentication" : "Autenticazione fallita. ${state_error}", - "wallet_menu" : "Menu", + "wallet_menu" : "Menù", "Blocks_remaining" : "${status} Blocchi Rimanenti", "please_try_to_connect_to_another_node" : "Gentilmente prova a connetterti ad un altro nodo", "xmr_hidden" : "Nascosto", "xmr_available_balance" : "Saldo Disponibile", - "xmr_full_balance" : "Saldo Completo", + "xmr_full_balance" : "Saldo Totale", "send" : "Invia", "receive" : "Ricevi", "transactions" : "Transazioni", @@ -53,7 +53,7 @@ "yesterday" : "Ieri", "received" : "Ricevuto", "sent" : "Inviato", - "pending" : " (pendente)", + "pending" : " (non confermati)", "rescan" : "Scansiona di nuovo", "reconnect" : "Riconnetti", "wallets" : "Portafogli", @@ -70,19 +70,19 @@ "change_exchange_provider" : "Cambia Exchange", "you_will_send" : "Conveti da", "you_will_get" : "Converti a", - "amount_is_guaranteed" : "L'ammonare da ricevere è fissato", + "amount_is_guaranteed" : "L'ammontare da ricevere è fisso", "amount_is_estimate" : "L'ammontare da ricevere è una stima", "powered_by" : "Sviluppato da ${title}", "error" : "Errore", "estimated" : "Stimato", "min_value" : "Min: ${value} ${currency}", "max_value" : "Max: ${value} ${currency}", - "change_currency" : "Cambia Moneta", - "overwrite_amount" : "Overwrite amount", - "qr_payment_amount" : "This QR code contains a payment amount. Do you want to overwrite the current value?", + "change_currency" : "Cambia Valuta", + "overwrite_amount" : "Sovrascrivi quantità", + "qr_payment_amount" : "Questo codice QR contiene l'ammontare del pagamento. Vuoi sovrascrivere il varlore attuale?", "copy_id" : "Copia ID", - "exchange_result_write_down_trade_id" : "Gentilmente fai una copia o trascrivi l'ID dello scambio per continuare.", + "exchange_result_write_down_trade_id" : "Per favore fai una copia o trascrivi l'ID dello scambio per continuare.", "trade_id" : "ID Scambio:", "copied_to_clipboard" : "Copiato negli Appunti", "saved_the_trade_id" : "Ho salvato l'ID dello scambio", @@ -183,7 +183,7 @@ "restore_from_date_or_blockheight" : "Gentilmente inserisci la data di un paio di giorni prima che hai creato questo portafoglio. Oppure inserisci l'altezza del blocco se la conosci", - "seed_reminder" : "Gentilmente trascrivi le parole. Ti tornerà utie in caso perdessi o ripristinassi il tuo telefono", + "seed_reminder" : "Gentilmente trascrivi le parole. Ti tornerà utile in caso perdessi o ripristinassi il tuo telefono", "seed_title" : "Seme", "seed_share" : "Condividi seme", "copy" : "Copia", @@ -201,7 +201,7 @@ "seed_language_russian" : "Russo", "seed_language_spanish" : "Spagnolo", "seed_language_french": "Francese", - "seed_language_italian": "Italiana/Italiano", + "seed_language_italian": "Italiano", "send_title" : "Invia", @@ -317,13 +317,13 @@ "router_no_route" : "Nessun percorso definito per ${name}", - "error_text_account_name" : "Il nome dell'Account può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", + "error_text_account_name" : "Il nome del conto può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", "error_text_contact_name" : "Il nome del Contatto non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 32 caratteri", "error_text_address" : "L'indirizzo del Portafoglio deve corrispondere alla tipologia\ndi criptovaluta", "error_text_node_address" : "Gentilmente inserisci un indirizzo iPv4", "error_text_node_port" : "La porta del nodo può contenere solo numeri compresi tra 0 e 65535", - "error_text_payment_id" : "l'ID del pagamento può contenere solo da 16 a 64 caratteri in hex", - "error_text_xmr" : "Il valore XMR non può eccedere il saldo disponibile.\nIl numero delle cifre decimali deve essere inferiore o uguale a 12", + "error_text_payment_id" : "L'ID del pagamento può contenere solo da 16 a 64 caratteri in esadecimale", + "error_text_xmr" : "Il valore in XMR non può eccedere il saldo disponibile.\nIl numero delle cifre decimali deve essere inferiore o uguale a 12", "error_text_fiat" : "L'ammontare non può eccedere il saldo dispoinibile.\nIl numero di cifre decimali deve essere inferiore o uguale a 2", "error_text_subaddress_name" : "Il nome del sottoindirizzo non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 20 caratteri", "error_text_amount" : "L'ammontare può contenere solo numeri", @@ -337,7 +337,7 @@ "auth_store_ban_timeout" : "ban_timeout", - "auth_store_banned_for" : "Bannato per ", + "auth_store_banned_for" : "Bloccato per ", "auth_store_banned_minutes" : " minuti", "auth_store_incorrect_password" : "PIN non corretto", "wallet_store_monero_wallet" : "Portafoglio Monero", @@ -387,7 +387,7 @@ "trade_state_finished" : "Finito", "change_language" : "Cambia lingua", - "change_language_to" : "Cambia lingua in ${language}?", + "change_language_to" : "Cambiare lingua in ${language}?", "paste" : "Incolla", "restore_from_seed_placeholder" : "Gentilmente inserisci o incolla il tuo seme qui", @@ -401,7 +401,7 @@ "openalias_alert_content" : "Invierai i tuoi fondi a\n${recipient_name}", "card_address" : "Indirizzo:", - "buy" : "Compra", + "buy" : "Comprare", "sell": "Vendere", "placeholder_transactions" : "Le tue transazioni saranno mostrate qui", @@ -510,7 +510,7 @@ "add_receiver" : "Aggiungi un altro ricevitore (opzionale)", - "manage_yats" : "Gestisci Yats", + "manage_yats" : "Gestisci gli Yat", "yat_alert_title" : "Invia e ricevi criptovalute più facilmente con Yat", "yat_alert_content" : "Gli utenti di Cake Wallet possono ora inviare e ricevere tutte le loro valute preferite con un nome utente unico basato su emoji.", "get_your_yat" : "Ottieni il tuo Yat", @@ -525,9 +525,9 @@ "yat_popup_title" : "L'indirizzo del tuo portafoglio può essere emoji.", "yat_popup_content" : "Ora puoi inviare e ricevere criptovalute in Cake Wallet con il tuo Yat, un breve nome utente basato su emoji. Gestisci Yats in qualsiasi momento nella schermata delle impostazioni", "second_intro_title" : "Un indirizzo emoji per domarli tutti", - "second_intro_content" : "Il tuo Yat è un unico indirizzo emoji univoco che sostituisce tutti i tuoi lunghi indirizzi esadecimali per tutte le tue valute.", + "second_intro_content" : "Il tuo Yat è un unico indirizzo emoji univoco che sostituisce i lunghi indirizzi per tutte le tue valute.", "third_intro_title" : "Yat gioca bene con gli altri", - "third_intro_content" : "Anche Yats vive fuori da Cake Wallet. Qualsiasi indirizzo di portafoglio sulla terra può essere sostituito con un Yat!", + "third_intro_content" : "Yat può funzionare anche fuori da Cake Wallet. Qualsiasi indirizzo di portafoglio sulla terra può essere sostituito con uno Yat!", "learn_more" : "Impara di più", "search": "Ricerca", "search_language": "Cerca lingua", From 424cf25052f4f3a826140f3a82da6ff15c40d0f2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 2 Sep 2022 18:04:19 +0300 Subject: [PATCH 51/87] =?UTF-8?q?=D0=A1ake=20pay=20filter=20white=20screen?= =?UTF-8?q?=20bug=20(#491)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * increase touch area for filter button * rework card list view model * fix button color * fix formating --- lib/di.dart | 4 -- .../ionia/cards/ionia_manage_cards_page.dart | 55 ++++++++--------- .../ionia/widgets/ionia_filter_modal.dart | 30 ++++------ .../ionia/ionia_filter_view_model.dart | 58 ------------------ .../ionia_gift_cards_list_view_model.dart | 59 +++++++++++++++---- 5 files changed, 87 insertions(+), 119 deletions(-) delete mode 100644 lib/view_model/ionia/ionia_filter_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index 02a86d1e5..5cc698d27 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; -import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; @@ -12,7 +11,6 @@ import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/ionia/ionia_merchant.dart'; @@ -686,8 +684,6 @@ Future setup( getIt.get(), getIt.get(), getIt.get().wallet)); - - getIt.registerFactory(() => IoniaFilterViewModel()); getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart index f8b28e69e..a71dae087 100644 --- a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -1,5 +1,3 @@ -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/ionia/ionia_create_state.dart'; import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/routes.dart'; @@ -13,7 +11,6 @@ import 'package:cake_wallet/utils/debounce.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -28,6 +25,9 @@ class IoniaManageCardsPage extends BasePage { }); } }); + + _cardsListViewModel.getMerchants(); + } final IoniaGiftCardsListViewModel _cardsListViewModel; @@ -108,15 +108,27 @@ class IoniaManageCardsPage extends BasePage { @override Widget body(BuildContext context) { - final filterIcon = InkWell( + final filterButton = InkWell( onTap: () async { - final selectedFilters = await showCategoryFilter(context, _cardsListViewModel); - _cardsListViewModel.setSelectedFilter(selectedFilters); + await showCategoryFilter(context); + _cardsListViewModel.getMerchants(); }, - child: Image.asset( - 'assets/images/filter.png', - color: Theme.of(context).textTheme.caption.decorationColor, - )); + child: Container( + width: 32, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + child: Image.asset( + 'assets/images/filter.png', + color: Theme.of(context).textTheme.caption.decorationColor, + ), + ) + ); return Padding( padding: const EdgeInsets.all(14.0), @@ -132,18 +144,7 @@ class IoniaManageCardsPage extends BasePage { controller: _searchController, )), SizedBox(width: 10), - Container( - width: 32, - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).textTheme.title.backgroundColor, - border: Border.all( - color: Colors.white.withOpacity(0.2), - ), - borderRadius: BorderRadius.circular(10), - ), - child: filterIcon, - ) + filterButton ], ), ), @@ -158,16 +159,12 @@ class IoniaManageCardsPage extends BasePage { ); } - Future> showCategoryFilter( - BuildContext context, - IoniaGiftCardsListViewModel viewModel, - ) async { - return await showPopUp>( + Future showCategoryFilter(BuildContext context) async { + return showPopUp( context: context, builder: (BuildContext context) { return IoniaFilterModal( - filterViewModel: getIt.get(), - selectedCategories: viewModel.selectedFilters, + ioniaGiftCardsListViewModel: _cardsListViewModel, ); }, ); diff --git a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart index 242b19e51..fe987cf30 100644 --- a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart +++ b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart @@ -1,22 +1,18 @@ -import 'package:cake_wallet/ionia/ionia_category.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/palette.dart'; class IoniaFilterModal extends StatelessWidget { - IoniaFilterModal({ - @required this.filterViewModel, - @required this.selectedCategories, - }) { - filterViewModel.setSelectedCategories(this.selectedCategories); + IoniaFilterModal({@required this.ioniaGiftCardsListViewModel}){ + ioniaGiftCardsListViewModel.resetIoniaCategories(); } - final IoniaFilterViewModel filterViewModel; - final List selectedCategories; + final IoniaGiftCardsListViewModel ioniaGiftCardsListViewModel; @override Widget build(BuildContext context) { @@ -48,7 +44,7 @@ class IoniaFilterModal extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(left: 24, right: 24), child: TextField( - onChanged: filterViewModel.onSearchFilter, + onChanged: ioniaGiftCardsListViewModel.onSearchFilter, style: textMedium( color: Theme.of(context).primaryTextTheme.title.color, ), @@ -73,13 +69,13 @@ class IoniaFilterModal extends StatelessWidget { return ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, - itemCount: filterViewModel.ioniaCategories.length, + itemCount: ioniaGiftCardsListViewModel.ioniaCategories.length, itemBuilder: (_, index) { - final category = filterViewModel.ioniaCategories[index]; + final category = ioniaGiftCardsListViewModel.ioniaCategories[index]; return Padding( padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), child: InkWell( - onTap: () => filterViewModel.selectFilter(category), + onTap: () => ioniaGiftCardsListViewModel.setSelectedFilter(category), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -98,9 +94,9 @@ class IoniaFilterModal extends StatelessWidget { ], ), Observer(builder: (_) { - final value = filterViewModel.selectedIndices; + final value = ioniaGiftCardsListViewModel.selectedIndices; return RoundedCheckbox( - value: value.contains(category.index), + value: value.contains(category), ); }), ], @@ -114,13 +110,13 @@ class IoniaFilterModal extends StatelessWidget { ), ), InkWell( - onTap: () => Navigator.pop(context, filterViewModel.selectedCategories), + onTap: () => Navigator.pop(context), child: Container( margin: EdgeInsets.only(bottom: 40), child: CircleAvatar( child: Icon( Icons.close, - color: Colors.black, + color: Palette.darkBlueCraiola, ), backgroundColor: Colors.white, ), diff --git a/lib/view_model/ionia/ionia_filter_view_model.dart b/lib/view_model/ionia/ionia_filter_view_model.dart deleted file mode 100644 index 43d2790e0..000000000 --- a/lib/view_model/ionia/ionia_filter_view_model.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_category.dart'; -import 'package:mobx/mobx.dart'; - -part 'ionia_filter_view_model.g.dart'; - -class IoniaFilterViewModel = IoniaFilterViewModelBase with _$IoniaFilterViewModel; - -abstract class IoniaFilterViewModelBase with Store { - IoniaFilterViewModelBase() { - selectedIndices = ObservableList(); - ioniaCategories = IoniaCategory.allCategories; - } - - List get selectedCategories => ioniaCategories.where(_isSelected).toList(); - - @observable - ObservableList selectedIndices; - - @observable - List ioniaCategories; - - @action - void selectFilter(IoniaCategory ioniaCategory) { - if (ioniaCategory == IoniaCategory.all && !selectedIndices.contains(0)) { - selectedIndices.clear(); - selectedIndices.add(0); - return; - } - if (selectedIndices.contains(ioniaCategory.index) && ioniaCategory.index != 0) { - selectedIndices.remove(ioniaCategory.index); - return; - } - selectedIndices.add(ioniaCategory.index); - selectedIndices.remove(0); - } - - @action - void onSearchFilter(String text) { - if (text.isEmpty) { - ioniaCategories = IoniaCategory.allCategories; - } else { - ioniaCategories = IoniaCategory.allCategories - .where( - (e) => e.title.toLowerCase().contains(text.toLowerCase()), - ) - .toList(); - } - } - - @action - void setSelectedCategories(List selectedCategories) { - selectedIndices = ObservableList.of(selectedCategories.map((e) => e.index)); - } - - bool _isSelected(IoniaCategory ioniaCategory) { - return selectedIndices.contains(ioniaCategory.index); - } -} diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart index a04fce3e4..ec603dd89 100644 --- a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart @@ -12,14 +12,13 @@ class IoniaGiftCardsListViewModel = IoniaGiftCardsListViewModelBase with _$Ionia abstract class IoniaGiftCardsListViewModelBase with Store { IoniaGiftCardsListViewModelBase({ @required this.ioniaService, - }) : + }) : cardState = IoniaNoCardState(), ioniaMerchants = [], + ioniaCategories = IoniaCategory.allCategories, + selectedIndices = ObservableList.of([IoniaCategory.all]), scrollOffsetFromTop = 0.0 { - selectedFilters = []; _getAuthStatus().then((value) => isLoggedIn = value); - - _getMerchants(); } final IoniaService ioniaService; @@ -28,8 +27,6 @@ abstract class IoniaGiftCardsListViewModelBase with Store { String searchString; - List selectedFilters; - @observable double scrollOffsetFromTop; @@ -48,6 +45,12 @@ abstract class IoniaGiftCardsListViewModelBase with Store { @observable bool isLoggedIn; + @observable + List ioniaCategories; + + @observable + ObservableList selectedIndices; + Future _getAuthStatus() async { return await ioniaService.isLogined(); } @@ -88,9 +91,10 @@ abstract class IoniaGiftCardsListViewModelBase with Store { } } - void _getMerchants() { + + void getMerchants() { merchantState = IoniaLoadingMerchantState(); - ioniaService.getMerchantsByFilter(categories: selectedFilters).then((value) { + ioniaService.getMerchantsByFilter(categories: selectedIndices).then((value) { value.sort((a, b) => a.legalName.toLowerCase().compareTo(b.legalName.toLowerCase())); ioniaMerchants = ioniaMerchantList = value; merchantState = IoniaLoadedMerchantState(); @@ -99,9 +103,42 @@ abstract class IoniaGiftCardsListViewModelBase with Store { } @action - void setSelectedFilter(List filters) { - selectedFilters = filters; - _getMerchants(); + void setSelectedFilter(IoniaCategory category) { + if (category == IoniaCategory.all) { + selectedIndices.clear(); + selectedIndices.add(category); + return; + } + + if (category != IoniaCategory.all) { + selectedIndices.remove(IoniaCategory.all); + } + + if (selectedIndices.contains(category)) { + selectedIndices.remove(category); + + if (selectedIndices.isEmpty) { + selectedIndices.add(IoniaCategory.all); + } + return; + } + selectedIndices.add(category); + } + + @action + void onSearchFilter(String text) { + if (text.isEmpty) { + ioniaCategories = IoniaCategory.allCategories; + } else { + ioniaCategories = IoniaCategory.allCategories + .where((e) => e.title.toLowerCase().contains(text.toLowerCase()),) + .toList(); + } + } + + @action + void resetIoniaCategories() { + ioniaCategories = IoniaCategory.allCategories; } void setScrollOffsetFromTop(double scrollOffset) { From 244d20d1b66287b329e9ef8210e8a1836a8d119d Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 7 Sep 2022 13:43:03 +0200 Subject: [PATCH 52/87] Skip unsupported providers when calculating best rate provider (#500) --- lib/view_model/exchange/exchange_view_model.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d55d4f9df..8ae653e5a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -210,6 +210,10 @@ abstract class ExchangeViewModelBase with Store { currentTradeAvailableProviders.clear(); for (var provider in selectedProviders) { + /// if this provider is not valid for the current pair, skip it + if (!providersForCurrentPair().contains(provider)) { + continue; + } provider .calculateAmount( from: receiveCurrency, @@ -232,8 +236,8 @@ abstract class ExchangeViewModelBase with Store { isFixedRateMode: isFixedRateMode, ).then((limits) { /// if the entered amount doesn't exceed the limits of this provider - if ((limits.max ?? double.maxFinite) >= _enteredAmount - && (limits.min ?? 0) <= _enteredAmount) { + if ((limits?.max ?? double.maxFinite) >= _enteredAmount + && (limits?.min ?? 0) <= _enteredAmount) { /// add this provider as its valid for this trade /// will be sorted ascending already since /// we seek the least deposit amount @@ -263,6 +267,10 @@ abstract class ExchangeViewModelBase with Store { currentTradeAvailableProviders.clear(); for (var provider in selectedProviders) { + /// if this provider is not valid for the current pair, skip it + if (!providersForCurrentPair().contains(provider)) { + continue; + } provider .calculateAmount( from: depositCurrency, @@ -286,8 +294,8 @@ abstract class ExchangeViewModelBase with Store { ).then((limits) { /// if the entered amount doesn't exceed the limits of this provider - if ((limits.max ?? double.maxFinite) >= _enteredAmount - && (limits.min ?? 0) <= _enteredAmount) { + if ((limits?.max ?? double.maxFinite) >= _enteredAmount + && (limits?.min ?? 0) <= _enteredAmount) { /// add this provider as its valid for this trade /// subtract from maxFinite so the provider /// with the largest amount would be sorted ascending From bed815e370fee247169620ebe29d3570a17d2e27 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:58:37 +0300 Subject: [PATCH 53/87] update ui bug (#497) --- lib/src/screens/ionia/cards/ionia_account_cards_page.dart | 1 + lib/src/screens/ionia/cards/ionia_account_page.dart | 7 +++++-- lib/src/screens/ionia/cards/ionia_buy_gift_card.dart | 2 -- lib/src/screens/ionia/widgets/card_item.dart | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart index bcce3bf13..5e566b8c1 100644 --- a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart @@ -179,6 +179,7 @@ class _IoniaCardListView extends StatelessWidget { title: merchant.legalName, backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), discount: 0, + hideBorder: true, discountBackground: AssetImage('assets/images/red_badge_discount.png'), titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor, subtitleColor: Theme.of(context).hintColor, diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart index 817f966fa..5d985fd25 100644 --- a/lib/src/screens/ionia/cards/ionia_account_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_page.dart @@ -169,8 +169,11 @@ class _GradiantContainer extends StatelessWidget { borderRadius: BorderRadius.circular(15), gradient: LinearGradient( colors: [ - Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).accentColor, + Theme.of(context) + .primaryTextTheme + .subhead + .decorationColor, + Theme.of(context).primaryTextTheme.subhead.color, ], begin: Alignment.topRight, end: Alignment.bottomLeft, diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart index 015ded0e8..0cd8354ef 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; @@ -8,7 +7,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart index a180c2bdd..6c5d3a639 100644 --- a/lib/src/screens/ionia/widgets/card_item.dart +++ b/lib/src/screens/ionia/widgets/card_item.dart @@ -8,6 +8,7 @@ class CardItem extends StatelessWidget { @required this.backgroundColor, @required this.titleColor, @required this.subtitleColor, + this.hideBorder = false, this.discountBackground, this.onTap, this.logoUrl, @@ -21,6 +22,7 @@ class CardItem extends StatelessWidget { final String logoUrl; final double discount; final bool isAmount; + final bool hideBorder; final Color backgroundColor; final Color titleColor; final Color subtitleColor; @@ -38,7 +40,7 @@ class CardItem extends StatelessWidget { decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(20), - border: Border.all( + border: hideBorder ? Border.symmetric(horizontal: BorderSide.none, vertical: BorderSide.none) : Border.all( color: Colors.white.withOpacity(0.20), ), ), From f59c2fbd2be9828c11d6931ee6a4ce8e07f3334f Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 7 Sep 2022 15:10:21 +0300 Subject: [PATCH 54/87] add new coins to exchange (#499) * add new coins APE, AVAXC, BTT, BTTBSC, DOGE, FIRO, USDTTRC20, HBAR, SC, SOL, USDC, USDCSOL, ZEN, XVG * remove UST icon * remove xhv from excludeDepositCurrencies * remove xhv from excludeReceiveCurrencies --- assets/images/ape_icon.png | Bin 0 -> 29337 bytes assets/images/avaxc_icon.png | Bin 0 -> 4184 bytes assets/images/btt_icon.png | Bin 0 -> 10730 bytes assets/images/bttbsc_icon.png | Bin 0 -> 10730 bytes assets/images/doge_icon.png | Bin 0 -> 17195 bytes assets/images/firo_icon.png | Bin 0 -> 5463 bytes assets/images/hbar_icon.png | Bin 0 -> 3683 bytes assets/images/nano_icon.png | Bin 0 -> 4979 bytes assets/images/sc_icon.png | Bin 0 -> 5519 bytes assets/images/sol_icon.png | Bin 0 -> 6754 bytes assets/images/usdc_icon.png | Bin 0 -> 6923 bytes assets/images/usdcsol_icon.png | Bin 0 -> 6923 bytes assets/images/usdttrc20_icon.png | Bin 0 -> 3177 bytes assets/images/xvg_icon.png | Bin 0 -> 8083 bytes assets/images/zaddr_icon.png | Bin 16312 -> 6158 bytes assets/images/zec_icon.png | Bin 16312 -> 6158 bytes assets/images/zen_icon.png | Bin 0 -> 10640 bytes cw_core/lib/crypto_currency.dart | 109 ++++++++++++++++-- .../changenow_exchange_provider.dart | 6 +- .../widgets/currency_picker_item_widget.dart | 1 - .../exchange/exchange_view_model.dart | 5 +- 21 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 assets/images/ape_icon.png create mode 100644 assets/images/avaxc_icon.png create mode 100644 assets/images/btt_icon.png create mode 100644 assets/images/bttbsc_icon.png create mode 100644 assets/images/doge_icon.png create mode 100644 assets/images/firo_icon.png create mode 100644 assets/images/hbar_icon.png create mode 100644 assets/images/nano_icon.png create mode 100644 assets/images/sc_icon.png create mode 100644 assets/images/sol_icon.png create mode 100644 assets/images/usdc_icon.png create mode 100644 assets/images/usdcsol_icon.png create mode 100644 assets/images/usdttrc20_icon.png create mode 100644 assets/images/xvg_icon.png create mode 100644 assets/images/zen_icon.png diff --git a/assets/images/ape_icon.png b/assets/images/ape_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c3d9c3464b73822e070a50f447b6cefdf5a38a GIT binary patch literal 29337 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 literal 0 HcmV?d00001 diff --git a/assets/images/firo_icon.png b/assets/images/firo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..076d8421b261b85cfc30b8578dd42a007449a9cc GIT binary patch literal 5463 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 M>ay8c>KK~%FDusbs{jB1 literal 0 HcmV?d00001 diff --git a/assets/images/nano_icon.png b/assets/images/nano_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8937b8ed293a23d127cee5c71b30d1731d3f6707 GIT binary patch literal 4979 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 literal 0 HcmV?d00001 diff --git a/assets/images/sc_icon.png b/assets/images/sc_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff4b2e21bf60ea4dd0cc4328a624c029d252f81 GIT binary patch literal 5519 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 Ef}prjkN^Mx literal 0 HcmV?d00001 diff --git a/assets/images/usdc_icon.png b/assets/images/usdc_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..283533b42a38d4fd8b1465233d9afb073a67189f GIT binary patch literal 6923 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

T`POuqVfY6Bj06`o1HX#d-jO0O#xsU+3 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 literal 0 HcmV?d00001 diff --git a/assets/images/zaddr_icon.png b/assets/images/zaddr_icon.png index 139acf84c54f2e3622db5346db88043071dea2c2..095ad2c560c70f6a043c2db8f652574783f86254 100644 GIT binary patch literal 6158 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 Zso}Sq{Fyg?a{MECqG6z3{@5<;e*gqh6AJ(U diff --git a/assets/images/zec_icon.png b/assets/images/zec_icon.png index 139acf84c54f2e3622db5346db88043071dea2c2..095ad2c560c70f6a043c2db8f652574783f86254 100644 GIT binary patch literal 6158 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 Zso}Sq{Fyg?a{MECqG6z3{@5<;e*gqh6AJ(U diff --git a/assets/images/zen_icon.png b/assets/images/zen_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2060a0d22fc8073c9863de92144fd315ca46fbd5 GIT binary patch literal 10640 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 with Serializable { CryptoCurrency.eos, CryptoCurrency.eth, CryptoCurrency.ltc, + CryptoCurrency.nano, CryptoCurrency.trx, CryptoCurrency.usdt, CryptoCurrency.usdterc20, CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.xhv, - //CryptoCurrency.zaddr, - //CryptoCurrency.zec + CryptoCurrency.ape, + CryptoCurrency.avaxc, + CryptoCurrency.btt, + CryptoCurrency.bttbsc, + CryptoCurrency.doge, + CryptoCurrency.firo, + CryptoCurrency.usdttrc20, + CryptoCurrency.hbar, + CryptoCurrency.sc, + CryptoCurrency.sol, + CryptoCurrency.usdc, + CryptoCurrency.usdcsol, + CryptoCurrency.zaddr, + CryptoCurrency.zec, + CryptoCurrency.zen, + CryptoCurrency.xvg, ]; - static const xmr = CryptoCurrency(title: 'XMR', iconPath: 'assets/images/monero_icon.png', name: 'Monero', raw: 0); + + static const xmr = CryptoCurrency(title: 'XMR', iconPath: 'assets/images/monero_icon.png', name: 'Monero', raw: 0); static const ada = CryptoCurrency(title: 'ADA', iconPath: 'assets/images/ada_icon.png', name: 'Cardano', raw: 1); static const bch = CryptoCurrency(title: 'BCH', iconPath: 'assets/images/bch_icon.png',name: 'Bitcoin Cash', raw: 2); static const bnb = CryptoCurrency(title: 'BNB', iconPath: 'assets/images/bnb_icon.png', tag: 'BSC', name: 'Binance Coin', raw: 3); @@ -41,7 +57,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const dash = CryptoCurrency(title: 'DASH', iconPath: 'assets/images/dash_icon.png', name: 'Dash', raw: 6); static const eos = CryptoCurrency(title: 'EOS', iconPath: 'assets/images/eos_icon.png', name: 'EOS', raw: 7); static const eth = CryptoCurrency(title: 'ETH', iconPath: 'assets/images/eth_icon.png', name: 'Ethereum', raw: 8); - static const ltc = CryptoCurrency(title: 'LTC', iconPath: 'assets/images/litecoin-ltc_icon.png', name: 'Litecoin',raw: 9); + static const ltc = CryptoCurrency(title: 'LTC', iconPath: 'assets/images/litecoin-ltc_icon.png', name: 'Litecoin', raw: 9); static const nano = CryptoCurrency(title: 'NANO', raw: 10); static const trx = CryptoCurrency(title: 'TRX', iconPath: 'assets/images/trx_icon.png', name: 'TRON', raw: 11); static const usdt = CryptoCurrency(title: 'USDT', iconPath: 'assets/images/usdt_icon.png', tag: 'OMNI', name: 'USDT', raw: 12); @@ -64,8 +80,25 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28); static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29); - static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', name: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 30); - static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', name: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 31); + static const ape = CryptoCurrency(title: 'APE', iconPath: 'assets/images/ape_icon.png', raw: 30); + static const avaxc = CryptoCurrency(title: 'AVAXC', iconPath: 'assets/images/avaxc_icon.png', raw: 31); + static const btt = CryptoCurrency(title: 'BTT', iconPath: 'assets/images/btt_icon.png', raw: 32); + static const bttbsc = CryptoCurrency(title: 'BTTBSC', iconPath: 'assets/images/bttbsc_icon.png', raw: 33); + static const doge = CryptoCurrency(title: 'DOGE', iconPath: 'assets/images/doge_icon.png', raw: 34); + static const firo = CryptoCurrency(title: 'FIRO', iconPath: 'assets/images/firo_icon.png', raw: 35); + static const usdttrc20 = CryptoCurrency(title: 'USDTTRC20', iconPath: 'assets/images/usdttrc20_icon.png', raw: 36); + static const hbar = CryptoCurrency(title: 'HBAR', iconPath: 'assets/images/hbar_icon.png', raw: 37); + static const sc = CryptoCurrency(title: 'SC', iconPath: 'assets/images/sc_icon.png', raw: 38); + static const sol = CryptoCurrency(title: 'SOL', iconPath: 'assets/images/sol_icon.png', raw: 39); + static const usdc = CryptoCurrency(title: 'USDC', iconPath: 'assets/images/usdc_icon.png', raw: 40); + static const usdcsol = CryptoCurrency(title: 'USDCSOL', iconPath: 'assets/images/usdcsol_icon.png', raw: 41); + static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', name: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 42); + static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', name: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 43); + static const zen = CryptoCurrency(title: 'ZEN', iconPath: 'assets/images/zen_icon.png', raw: 44); + static const xvg = CryptoCurrency(title: 'XVG', name: 'Verge', iconPath: 'assets/images/xvg_icon.png', raw: 45); + + + static CryptoCurrency deserialize({int raw}) { switch (raw) { @@ -130,9 +163,37 @@ class CryptoCurrency extends EnumerableItem with Serializable { case 29: return CryptoCurrency.xusd; case 30: - return CryptoCurrency.zaddr; + return CryptoCurrency.ape; case 31: + return CryptoCurrency.avaxc; + case 32: + return CryptoCurrency.btt; + case 33: + return CryptoCurrency.bttbsc; + case 34: + return CryptoCurrency.doge; + case 35: + return CryptoCurrency.firo; + case 36: + return CryptoCurrency.usdttrc20; + case 37: + return CryptoCurrency.hbar; + case 38: + return CryptoCurrency.sc; + case 39: + return CryptoCurrency.sol; + case 40: + return CryptoCurrency.usdc; + case 41: + return CryptoCurrency.usdcsol; + case 42: + return CryptoCurrency.zaddr; + case 43: return CryptoCurrency.zec; + case 44: + return CryptoCurrency.zen; + case 45: + return CryptoCurrency.xvg; default: return null; } @@ -164,8 +225,8 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.nano; case 'trx': return CryptoCurrency.trx; - case 'usdt': - return CryptoCurrency.usdt; + case 'usdc': + return CryptoCurrency.usdc; case 'usdterc20': return CryptoCurrency.usdterc20; case 'xlm': @@ -200,10 +261,38 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.xnzd; case 'xusd': return CryptoCurrency.xusd; + case 'ape': + return CryptoCurrency.ape; + case 'avax': + return CryptoCurrency.avaxc; + case 'btt': + return CryptoCurrency.btt; + case 'bttbsc': + return CryptoCurrency.bttbsc; + case 'doge': + return CryptoCurrency.doge; + case 'firo': + return CryptoCurrency.firo; + case 'usdttrc20': + return CryptoCurrency.usdttrc20; + case 'hbar': + return CryptoCurrency.hbar; + case 'sc': + return CryptoCurrency.sc; + case 'sol': + return CryptoCurrency.sol; + case 'usdt': + return CryptoCurrency.usdt; + case 'usdcsol': + return CryptoCurrency.usdcsol; case 'zaddr': return CryptoCurrency.zaddr; case 'zec': return CryptoCurrency.zec; + case 'zen': + return CryptoCurrency.zen; + case 'xvg': + return CryptoCurrency.xvg; default: return null; } @@ -211,4 +300,4 @@ class CryptoCurrency extends EnumerableItem with Serializable { @override String toString() => title; -} +} \ No newline at end of file diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index e02ae66a9..c004f327a 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -96,12 +96,12 @@ class ChangeNowExchangeProvider extends ExchangeProvider { 'Content-Type': 'application/json'}; final flow = getFlow(isFixedRateMode); final body = { - 'fromCurrency': normalizeCryptoCurrency(_request.from), + 'fromCurrency': normalizeCryptoCurrency(_request.from), 'toCurrency': normalizeCryptoCurrency(_request.to), 'fromNetwork': networkFor(_request.from), 'toNetwork': networkFor(_request.to), - 'fromAmount': _request.fromAmount, - 'toAmount': _request.toAmount, + 'fromAmount': _request.fromAmount, + 'toAmount': _request.toAmount, 'address': _request.address, 'flow': flow, 'refundAddress': _request.refundAddress diff --git a/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart b/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart index b710494fd..a23b88c10 100644 --- a/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart +++ b/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart @@ -69,6 +69,5 @@ class PickerItemWidget extends StatelessWidget { ), ), ); - ; } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 8ae653e5a..557e3188d 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -40,8 +40,9 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, this.tradesStore, this._settingsStore, this.sharedPreferences) { - const excludeDepositCurrencies = []; - const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb]; + const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano]; + const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, + CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano]; providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()]; _initialPairBasedOnWallet(); currentTradeAvailableProviders = SplayTreeMap(); From 4ae69d0344d47449979d5ecdc7d64810d39f58e9 Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:46:13 +0100 Subject: [PATCH 55/87] Update text for cake pay learn more notification (#502) --- res/values/strings_de.arb | 2 +- res/values/strings_en.arb | 2 +- res/values/strings_es.arb | 2 +- res/values/strings_fr.arb | 2 +- res/values/strings_hi.arb | 2 +- res/values/strings_hr.arb | 2 +- res/values/strings_it.arb | 2 +- res/values/strings_ja.arb | 2 +- res/values/strings_ko.arb | 2 +- res/values/strings_nl.arb | 2 +- res/values/strings_pl.arb | 2 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 2 +- res/values/strings_uk.arb | 2 +- res/values/strings_zh.arb | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b4a3a923f..3457f8ab2 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -634,7 +634,7 @@ "contact_support": "Support kontaktieren", "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden", "introducing_cake_pay": "Einführung von Cake Pay!", - "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!", + "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "automatic": "Automatisch", "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c707b0d87..ef8e6e495 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -634,7 +634,7 @@ "contact_support": "Contact Support", "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time", "introducing_cake_pay": "Introducing Cake Pay!", - "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!", + "cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.", "automatic": "Automatic", "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index f46fab21c..7c996134e 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -634,7 +634,7 @@ "contact_support": "Contactar con Soporte", "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento", "introducing_cake_pay": "¡Presentamos Cake Pay!", - "cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!", + "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "automatic": "Automático", "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d7bd0c84a..405f9eb74 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -632,7 +632,7 @@ "contact_support": "Contacter l'assistance", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", "introducing_cake_pay": "Présentation de Cake Pay!", - "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !", + "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "automatic": "Automatique", "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", "variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 96e0dcb6a..83c0808c4 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -634,7 +634,7 @@ "contact_support": "सहायता से संपर्क करें", "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं", "introducing_cake_pay": "परिचय Cake Pay!", - "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!", + "cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।", "automatic": "स्वचालित", "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 00a5f04c0..97d4d335c 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -634,7 +634,7 @@ "contact_support": "Kontaktirajte podršku", "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", "introducing_cake_pay": "Predstavljamo Cake Pay!", - "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!", + "cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.", "automatic": "Automatski", "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index c88254f8c..cd779471f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -634,7 +634,7 @@ "contact_support": "Contatta l'assistenza", "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", "introducing_cake_pay": "Presentazione di Cake Pay!", - "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!", + "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", "automatic": "Automatico", "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e909311f6..2b3920862 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -634,7 +634,7 @@ "contact_support": "サポートに連絡する", "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。", "introducing_cake_pay": "序章Cake Pay!", - "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。", + "cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。", "automatic": "自動", "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 48db01c9a..85ece0603 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -634,7 +634,7 @@ "contact_support": "지원팀에 문의", "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", "introducing_cake_pay": "소개 Cake Pay!", - "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!", + "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", "automatic": "자동적 인", "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9e39b271e..15da6fd7b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -634,7 +634,7 @@ "contact_support": "Contact opnemen met ondersteuning", "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin", "introducing_cake_pay": "Introductie van Cake Pay!", - "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!", + "cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.", "automatic": "automatisch", "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 30eb1eaa3..26d8a38d4 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -634,7 +634,7 @@ "contact_support": "Skontaktuj się z pomocą techniczną", "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin", "introducing_cake_pay": "Przedstawiamy Ciasto Pay!", - "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!", + "cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.", "automatic": "Automatyczny", "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index eac4ac37c..bfb4a2d0a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -634,7 +634,7 @@ "contact_support": "Contatar Suporte", "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento", "introducing_cake_pay": "Apresentando o Cake Pay!", - "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!", + "cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.", "automatic": "Automático", "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index c82d8969a..94a5b3c0b 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -634,7 +634,7 @@ "contact_support": "Связаться со службой поддержки", "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.", "introducing_cake_pay": "Представляем Cake Pay!", - "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!", + "cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.", "automatic": "автоматический", "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index caac9111a..b139750be 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -633,7 +633,7 @@ "contact_support": "Звернутися до служби підтримки", "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin", "introducing_cake_pay": "Представляємо Cake Pay!", - "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!", + "cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.", "automatic": "Автоматичний", "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d8b156002..487c6be52 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -632,7 +632,7 @@ "contact_support": "联系支持", "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡", "introducing_cake_pay": "介绍 Cake Pay!", - "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!", + "cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。", "automatic": "自动的", "fixed_pair_not_supported": "所选交易所不支持此固定货币对", "variable_pair_not_supported": "所选交易所不支持此变量对", From f6a2b3c74b819e891aefaa62659629a5fa579c9d Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 7 Sep 2022 15:53:33 +0300 Subject: [PATCH 56/87] Cw 126 check validation for new currencies (#501) * update address validation xmr, ape, avaxc, eth, usdc, hbar, sc, sol, usdcsoul, usdttrc20, btt, bttsc, doge,firo, xvg, zen, tzec, zzec * fix formating --- lib/core/address_validator.dart | 50 +++++++++++++++++-- .../widgets/currency_picker_item_widget.dart | 2 +- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index fec250a08..b9d6cf0d6 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -13,10 +13,14 @@ class AddressValidator extends TextValidator { static String getPattern(CryptoCurrency type) { switch (type) { case CryptoCurrency.xmr: - return '[0-9a-zA-Z]'; + return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; case CryptoCurrency.ada: return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; + case CryptoCurrency.ape: + return '0x[0-9a-zA-Z]'; + case CryptoCurrency.avaxc: + return '0x[0-9a-zA-Z]'; case CryptoCurrency.bch: return '[0-9a-zA-Z]'; case CryptoCurrency.bnb: @@ -31,13 +35,15 @@ class AddressValidator extends TextValidator { case CryptoCurrency.eos: return '[0-9a-zA-Z]'; case CryptoCurrency.eth: - return '[0-9a-zA-Z]'; + return '0x[0-9a-zA-Z]'; case CryptoCurrency.ltc: return '[0-9a-zA-Z]'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.trx: return '[0-9a-zA-Z]'; + case CryptoCurrency.usdc: + return '0x[0-9a-zA-Z]'; case CryptoCurrency.usdt: return '[0-9a-zA-Z]'; case CryptoCurrency.usdterc20: @@ -62,6 +68,12 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xnzd: case CryptoCurrency.xusd: return '[0-9a-zA-Z]'; + case CryptoCurrency.hbar: + return '[0-9a-zA-Z.]'; + case CryptoCurrency.zaddr: + return '^zs[0-9a-zA-Z]{75}\&|^zc[0-9a-zA-Z]{93}\$'; + case CryptoCurrency.zec: + return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; default: return '[0-9a-zA-Z]'; } @@ -70,9 +82,13 @@ class AddressValidator extends TextValidator { static List getLength(CryptoCurrency type) { switch (type) { case CryptoCurrency.xmr: - return [95, 106]; + return null; case CryptoCurrency.ada: return null; + case CryptoCurrency.ape: + return [42]; + case CryptoCurrency.avaxc: + return [42]; case CryptoCurrency.bch: return [42]; case CryptoCurrency.bnb: @@ -91,12 +107,22 @@ class AddressValidator extends TextValidator { return [34, 43]; case CryptoCurrency.nano: return [64, 65]; + case CryptoCurrency.sc: + return [76]; + case CryptoCurrency.sol: + return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.trx: return [34]; + case CryptoCurrency.usdc: + return [42]; + case CryptoCurrency.usdcsol: + return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.usdt: return [34]; case CryptoCurrency.usdterc20: return [42]; + case CryptoCurrency.usdttrc20: + return [34]; case CryptoCurrency.xlm: return [56]; case CryptoCurrency.xrp: @@ -116,6 +142,24 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xnzd: case CryptoCurrency.xusd: return [98, 99, 106]; + case CryptoCurrency.btt: + return [34]; + case CryptoCurrency.bttbsc: + return [34]; + case CryptoCurrency.doge: + return [34]; + case CryptoCurrency.firo: + return [34]; + case CryptoCurrency.hbar: + return [4, 5, 6, 7, 8, 9, 10, 11]; + case CryptoCurrency.xvg: + return [34]; + case CryptoCurrency.zen: + return [35]; + case CryptoCurrency.zaddr: + return null; + case CryptoCurrency.zec: + return null; default: return []; } diff --git a/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart b/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart index a23b88c10..1c7fb81a3 100644 --- a/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart +++ b/lib/src/screens/exchange/widgets/currency_picker_item_widget.dart @@ -22,7 +22,7 @@ class PickerItemWidget extends StatelessWidget { children: [ Container( child: Image.asset( - iconPath, + iconPath ?? '', height: 20.0, width: 20.0, ), From 7aa5013b5db3a5603afae9376a0097cf37b85184 Mon Sep 17 00:00:00 2001 From: Mathias Herberts Date: Wed, 7 Sep 2022 15:13:54 +0200 Subject: [PATCH 57/87] Fixed French translations (#384) * Fixed French translations * Update strings_fr.arb Add Haven to first_wallet_text for FR Co-authored-by: mkyq <53115730+mkyq@users.noreply.github.com> --- assets/faq/faq_fr.json | 28 ++++---- res/values/strings_fr.arb | 138 +++++++++++++++++++------------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/assets/faq/faq_fr.json b/assets/faq/faq_fr.json index f1e3585bc..cc4e52873 100644 --- a/assets/faq/faq_fr.json +++ b/assets/faq/faq_fr.json @@ -5,31 +5,31 @@ }, { "question" : "Comment envoyer des Monero vers une plateforme d'échange qui nécessite un ID de paiement ?", - "answer" : "Appuyez sur le bouton envoyer de l'écran du Wallet. Ensuite, copiez l'adresse de dépôt de la plateforme d'échange et collez là dans le champ adresse. Puis, copiez l'ID de paiement fourni par la plateforme d'échange et copiez le dans le champ ID de paiement. Enfin, entrez le montant que vous souhaitez envoyer et vous êtes fin prêt !\n" + "answer" : "Appuyez sur le bouton envoyer de l'écran du Portefeuille (Wallet). Ensuite, copiez l'adresse de dépôt de la plateforme d'échange et collez là dans le champ adresse. Puis, copiez l'ID de paiement fourni par la plateforme d'échange et copiez le dans le champ ID de paiement. Enfin, entrez le montant que vous souhaitez envoyer et vous êtes fin prêt !\n" }, { "question" : "Que faire si j'oublie d'entrer l'ID de paiement quand j'envoie des Monero vers une plateforme d'échange ?", - "answer" : "Bien que notre service de support ne puisse pas vous aider directement pour ce type de souci, c'est un problème très courant que la plupart des plateformes d'échange ont l'habitude de gérer. Contactez simplement le support de la plateforme d'échange, expliquez que vous avez oublié d'inclure l'ID de paiement et envoyez leur l'ID de transaction comme preuve. Vous pouvez visualiser l'ID de transaction en tapant sur transaction sur l'écran de votre wallet.\n" + "answer" : "Bien que notre service de support ne puisse pas vous aider directement pour ce type de souci, c'est un problème très courant que la plupart des plateformes d'échange ont l'habitude de gérer. Contactez simplement le support de la plateforme d'échange, expliquez que vous avez oublié d'inclure l'ID de paiement et envoyez leur l'ID de transaction comme preuve. Vous pouvez visualiser l'ID de transaction en tapant sur transaction sur l'écran de votre portefeuille (wallet).\n" }, { - "question" : "Que signifient \"seed\" et \"clefs\" ?", - "answer" : "Vos clefs encodent l'information privée de votre wallet, ce sont elles qui permettent de dépenser vos fonds et de visualiser les transactions entrantes.\nVotre seed est simplement une version de votre clef privée sous une forme plus simple à recopier. Votre seed et vos clefs sont la même chose, juste sous des formes différentes !\nNE DONNEZ JAMAIS votre seed ou vos clefs à quiconque. Vos fonds seront dérobés si vous donner votre seed ou vos clefs. Merci d'écrire cependant votre seed et de le stocker en lieu sûr (afin de vous permettre de restaurer votre wallet si vous perdez votre téléphone.)\n" + "question" : "Que signifient \"phrase secrète (seed)\" et \"clefs\" ?", + "answer" : "Vos clefs encodent l'information privée de votre portefeuille (wallet), ce sont elles qui permettent de dépenser vos fonds et de visualiser les transactions entrantes.\nVotre phrase secrète (seed) est simplement une version de votre clef privée sous une forme plus simple à recopier. Votre phrase secrète et vos clefs sont la même chose, juste sous des formes différentes !\nNE DONNEZ JAMAIS votre phrase secrète ou vos clefs à quiconque. Vos fonds seront dérobés si vous donnez votre phrase secrète ou vos clefs. Merci d'écrire cependant votre phrase secrète et de la stocker en lieu sûr (afin de vous permettre de restaurer votre portefeuille si vous perdez votre téléphone.)\n" }, { - "question" : "Combien de wallets puis-je créer ?", - "answer" : "Il n'y a pas de limite ! Vous pouvez créer autant de wallets que vous le souhaitez.\n" + "question" : "Combien de portefeuilles (wallets) puis-je créer ?", + "answer" : "Il n'y a pas de limite ! Vous pouvez créer autant de portefeuilles (wallets) que vous le souhaitez.\n" }, { - "question" : "Commen puis-je restarurer mon wallet ?", - "answer" : "Appuyez sur le menu •••, sélectionnez Wallets, puis choisissez Restaurer un Wallet. Entrez alors votre seed (ou vos clefs), et de façon optionnelle une date antérieure à la première transaction de votre wallet (cela permettra d'accélérer le processus de synchronisation). Vous pourrez avoir besoin de maintenir l'application ouverte pendant 15 à 30 minutes afin de restaurer complètement votre wallet.\n" + "question" : "Commen puis-je restarurer mon portefeuille (wallet) ?", + "answer" : "Appuyez sur le menu •••, sélectionnez Portefeuilles (Wallets), puis choisissez Restaurer un Portefeuille. Entrez alors votre phrase secrète (seed) (ou vos clefs), et de façon optionnelle une date antérieure à la première transaction de votre portefeuille (cela permettra d'accélérer le processus de synchronisation). Vous pourrez avoir besoin de maintenir l'application ouverte pendant 15 à 30 minutes afin de restaurer complètement votre portefeuille.\n" }, { - "question" : "Que puis-je faire si je perds mon seed ?", - "answer" : "Si vous oubliez votre seed, il y a des chances que vous l'ayez inscrit quelque part. Vérifiez vos notes et regardez sur votre ordinateur. Si vous ne parvenez pas à le retrouver, il est possible que vous ayez effectué une sauvegarde de Cake Wallet (dans ce cas vous pourrez restaurer d'après cette sauvegarde). Si aucune des ces options ne convient, malheureusement il n'y a plus rien à faire, vos fonds sont définitivement perdus.\n" + "question" : "Que puis-je faire si je perds ma phrase secrète (seed) ?", + "answer" : "Si vous oubliez votre phrase secrète (seed), il y a des chances que vous l'ayez inscrite quelque part. Vérifiez vos notes et regardez sur votre ordinateur. Si vous ne parvenez pas à la retrouver, il est possible que vous ayez effectué une sauvegarde de Cake Wallet (dans ce cas vous pourrez restaurer d'après cette sauvegarde). Si aucune des ces options ne convient, malheureusement il n'y a plus rien à faire, vos fonds sont définitivement perdus.\n" }, { - "question" : "Collectez vous des informations à propos de mon wallet ?", - "answer" : "Cake Wallet NE COLLECTE PAS d'informations à propos de votre wallet. Nous sommes respectueux de votre intimité.\n" + "question" : "Collectez vous des informations à propos de mon portefeuille (wallet) ?", + "answer" : "Cake Wallet NE COLLECTE PAS d'informations à propos de votre portefeuille (wallet). Nous sommes respectueux de votre intimité.\n" }, { "question" : "Est il possible d'annuler une transaction ?", @@ -37,11 +37,11 @@ }, { "question" : "Que sont les sous-adresses, et comment s'en servir ?", - "answer" : "Une sous-adresse est une adresse unique que vous pouvez générer à tout moment. Les montants envoyés vers cette adresse arrivent toujours dans votre wallet principal, mais la personne qui vous envoie les fonds ne peut pas déterminer votre adresse principale. Les sous-adresses commencent par un 8.\nVous pouvez créer une nouvelle sous-adresse dans l'écran Réception en appuyant sur le + à côté du bouton Sous-Adresses. Entrez un nom pour la sous-adresse et appuyez sur Ajouter. Ensuite appuyez sur le nom de la sous-adresse quand vous souhaitez l'utiliser !\nSi vous êtres paranoïaque, vous devriez créer une nouvelle sous-adresse à chaque fois que vous voulez recevoir des Monero.\n" + "answer" : "Une sous-adresse est une adresse unique que vous pouvez générer à tout moment. Les montants envoyés vers cette adresse arrivent toujours dans votre portefeuille (wallet) principal, mais la personne qui vous envoie les fonds ne peut pas déterminer votre adresse principale. Les sous-adresses commencent par un 8.\nVous pouvez créer une nouvelle sous-adresse dans l'écran Réception en appuyant sur le + à côté du bouton Sous-Adresses. Entrez un nom pour la sous-adresse et appuyez sur Ajouter. Ensuite appuyez sur le nom de la sous-adresse quand vous souhaitez l'utiliser !\nSi vous êtres paranoïaque, vous devriez créer une nouvelle sous-adresse à chaque fois que vous voulez recevoir des Monero.\n" }, { "question" : "Qu'est-ce que l'ID de transaction ?", - "answer" : "Une empreinte (hash) de transaction, ou ID de transaction, est un moyen unique d'identifier une transaction. Chaque transaction a sa propre empreinte. Si vous deve fournir une empreinte de transaction à quelque'un, allez simplement sur l'écran principal du Wallet, appuyez sur la transaction puis appuyez longuement sur la section du haut et sélectionnez Copier.\n" + "answer" : "Une empreinte (hash) de transaction, ou ID de transaction, est un moyen unique d'identifier une transaction. Chaque transaction a sa propre empreinte. Si vous deve fournir une empreinte de transaction à quelque'un, allez simplement sur l'écran principal du Portefeuille (Wallet), appuyez sur la transaction puis appuyez longuement sur la section du haut et sélectionnez Copier.\n" }, { "question" : "Je n'ai pas reçu mes XMR ! Que puis-je faire ?", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 405f9eb74..7d5a3c74d 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -1,13 +1,13 @@ { "welcome" : "Bienvenue sur", "cake_wallet" : "Cake Wallet", - "first_wallet_text" : "Super portefeuille pour Monero, Bitcoin, Litecoin, et Haven", - "please_make_selection" : "Merci de faire un choix ci-dessous pour créer ou restaurer votre portefeuille.", - "create_new" : "Créer un Nouveau Portefeuille", - "restore_wallet" : "Restaurer un Portefeuille", + "first_wallet_text" : "Super portefeuille (wallet) pour Monero, Bitcoin, Litecoin et Haven", + "please_make_selection" : "Merci de faire un choix ci-dessous pour créer ou restaurer votre portefeuille (wallet).", + "create_new" : "Créer un Nouveau Portefeuille (Wallet)", + "restore_wallet" : "Restaurer un Portefeuille (Wallet)", "monero_com": "Monero.com par Cake Wallet", - "monero_com_wallet_text": "Super portefeuille pour Monero", + "monero_com_wallet_text": "Super portefeuille (wallet) pour Monero", "accounts" : "Comptes", @@ -54,9 +54,9 @@ "pending" : " (en attente)", "rescan" : "Analyser la blockchain", "reconnect" : "Reconnecter", - "wallets" : "Portefeuilles", - "show_seed" : "Visualiser la graine", - "show_keys" : "Visualiser graine/clefs", + "wallets" : "Portefeuilles (Wallets)", + "show_seed" : "Visualiser la phrase secrète (seed)", + "show_keys" : "Visualiser les /clefs", "address_book_menu" : "Carnet d'Adresses", "reconnection" : "Reconnexion", "reconnect_alert_text" : "Êtes vous certain de vouloir vous reconnecter ?", @@ -77,7 +77,7 @@ "max_value" : "Max: ${value} ${currency}", "change_currency" : "Changer de Devise", "overwrite_amount" : "Écraser montant", - "qr_payment_amount" : "Ce QR code contient un montant de paiement. Voulez-vous écraser la valeur actuelle?", + "qr_payment_amount" : "Ce QR code contient un montant de paiement. Voulez-vous écraser la valeur actuelle ?", "copy_id" : "Copier l'ID", "exchange_result_write_down_trade_id" : "Merci de copier ou d'écrire l'ID d'échange pour continuer.", @@ -92,7 +92,7 @@ "offer_expires_in" : "L'Offre expire dans : ", "trade_is_powered_by" : "Cet échange est proposé par ${provider}", "copy_address" : "Copier l'Adresse", - "exchange_result_confirm" : "En pressant confirmer, vous enverrez ${fetchingLabel} ${from} depuis votre portefeuille nommé ${walletName} vers l'adresse ci-dessous. Vous pouvez aussi envoyer depuis votre portefeuille externe vers l'adresse/QR code ci-dessous.\n\nMerci d'appuyer sur confirmer pour continuer ou retournez en arrière pour modifier les montants.", + "exchange_result_confirm" : "En pressant confirmer, vous enverrez ${fetchingLabel} ${from} depuis votre portefeuille (wallet) nommé ${walletName} vers l'adresse ci-dessous. Vous pouvez aussi envoyer depuis votre portefeuille externe vers l'adresse/QR code ci-dessous.\n\nMerci d'appuyer sur confirmer pour continuer ou retournez en arrière pour modifier les montants.", "exchange_result_description" : "Vous devez envoyer un minimum de ${fetchingLabel} ${from} à l'adresse indiquée page suivante. Si vous envoyez un montant inférieur à ${fetchingLabel} ${from} il pourrait ne pas être converti et ne pas être remboursé.", "exchange_result_write_down_ID" : "*Merci de copier ou écrire votre ID présenté ci-dessus.", "confirm" : "Confirmer", @@ -109,13 +109,13 @@ "enter_your_pin" : "Entrez votre code PIN", - "loading_your_wallet" : "Chargement de votre portefeuille", + "loading_your_wallet" : "Chargement de votre portefeuille (wallet)", - "new_wallet" : "Nouveau Portefeuille", - "wallet_name" : "Nom du Portefeuille", + "new_wallet" : "Nouveau Portefeuille (Wallet)", + "wallet_name" : "Nom du Portefeuille (Wallet)", "continue_text" : "Continuer", - "choose_wallet_currency" : "Merci de choisir la devise du portefeuille :", + "choose_wallet_currency" : "Merci de choisir la devise du portefeuille (wallet) :", "node_new" : "Nouveau Nœud", @@ -126,7 +126,7 @@ "nodes" : "Nœuds", "node_reset_settings_title" : "Réinitialisation des réglages", "nodes_list_reset_to_default_message" : "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "change_current_node" : "Êtes vous certain de vouloir changer le nœud actuel vers ${node}?", + "change_current_node" : "Êtes vous certain de vouloir changer le nœud actuel vers ${node} ?", "change" : "Changer", "remove_node" : "Supprimer le nœud", "remove_node_message" : "Êtes vous certain de vouloir supprimer le nœud sélectionné ?", @@ -156,39 +156,39 @@ "accounts_subaddresses" : "Comptes et sous-adresses", - "restore_restore_wallet" : "Restaurer le Portefeuille", - "restore_title_from_seed_keys" : "Restaurer depuis une graine ou des clefs", - "restore_description_from_seed_keys" : "Restaurez votre portefeuille depuis une graine ou des clefs que vous avez stockés en lieu sûr", + "restore_restore_wallet" : "Restaurer le Portefeuille (Wallet)", + "restore_title_from_seed_keys" : "Restaurer depuis une phrase secrète (seed) ou des clefs", + "restore_description_from_seed_keys" : "Restaurez votre portefeuille (wallet) depuis une phrase secrète (seed) ou des clefs que vous avez stockées en lieu sûr", "restore_next" : "Suivant", "restore_title_from_backup" : "Restaurer depuis une sauvegarde", "restore_description_from_backup" : "Vous pouvez restaurer l'intégralité de l'application Cake Wallet depuis un fichier de sauvegarde", - "restore_seed_keys_restore" : "Restaurer depuis Graine/Clefs", - "restore_title_from_seed" : "Restaurer depuis graine", - "restore_description_from_seed" : "Restaurez votre portefeuille depuis une combinaison de 25 ou 13 mots", + "restore_seed_keys_restore" : "Restaurer depuis Phrase secrète (seed)/Clefs", + "restore_title_from_seed" : "Restaurer depuis une phrase secrète (seed)", + "restore_description_from_seed" : "Restaurer votre portefeuille (wallet) depuis une phrase secrète (seed) de 25 ou 13 mots", "restore_title_from_keys" : "Restaurer depuis des clefs", - "restore_description_from_keys" : "Restaurer votre portefeuille d'après les séquences de touches générées d'après vos clefs privées", - "restore_wallet_name" : "Nom du portefeuille", + "restore_description_from_keys" : "Restaurer votre portefeuille (wallet) d'après les séquences de touches générées d'après vos clefs privées", + "restore_wallet_name" : "Nom du portefeuille (wallet)", "restore_address" : "Adresse", "restore_view_key_private" : "Clef d'audit (view key) (privée)", "restore_spend_key_private" : "Clef de dépense (spend key) (privée)", "restore_recover" : "Restaurer", - "restore_wallet_restore_description" : "Description de la restauration de portefeuille", - "restore_new_seed" : "Nouvelle graine", - "restore_active_seed" : "Graine actif", - "restore_bitcoin_description_from_seed" : "Restaurer votre portefeuille à l'aide d'une combinaison de 24 mots", - "restore_bitcoin_description_from_keys" : "Restaurer votre portefeuille d'après la chaîne WIF générée d'après vos clefs privées", + "restore_wallet_restore_description" : "Description de la restauration de portefeuille (wallet)", + "restore_new_seed" : "Nouvelle phrase secrète (seed)", + "restore_active_seed" : "Phrase secrète (seed) active", + "restore_bitcoin_description_from_seed" : "Restaurer votre portefeuille (wallet) à l'aide d'une phrase secrète (seed) de 24 mots", + "restore_bitcoin_description_from_keys" : "Restaurer votre portefeuille (wallet) d'après la chaîne WIF générée d'après vos clefs privées", "restore_bitcoin_title_from_keys" : "Restaurer depuis la chaîne WIF", - "restore_from_date_or_blockheight" : "Merci d'entrer une date antérieure de quelques jours à la date de création de votre portefeuille. Ou si vous connaissez la hauteur de bloc, merci de la spécifier plutôt qu'une date", + "restore_from_date_or_blockheight" : "Merci d'entrer une date antérieure de quelques jours à la date de création de votre portefeuille (wallet). Ou si vous connaissez la hauteur de bloc, merci de la spécifier plutôt qu'une date", - "seed_reminder" : "Merci d'écrire votre graine au cas où vous perdriez ou effaceriez votre téléphone", - "seed_title" : "Graine", - "seed_share" : "Partager la graine", + "seed_reminder" : "Merci d'écrire votre phrase secrète (seed) au cas où vous perdriez ou effaceriez votre téléphone", + "seed_title" : "Phrase secrète (seed)", + "seed_share" : "Partager la phrase secrète (seed)", "copy" : "Copier", - "seed_language_choose" : "Merci de choisir la langue de la graine :", - "seed_choose" : "Choisissez la langue de la graine", + "seed_language_choose" : "Merci de choisir la langue de la phrase secrète (seed) :", + "seed_choose" : "Choisissez la langue de la phrase secrète (seed)", "seed_language_next" : "Suivant", "seed_language_english" : "Anglais", "seed_language_chinese" : "Chinois", @@ -203,7 +203,7 @@ "send_title" : "Envoyer", - "send_your_wallet" : "Votre portefeuille", + "send_your_wallet" : "Votre portefeuille (wallet)", "send_address" : "adresse ${cryptoCurrency}", "send_payment_id" : "ID de paiement (optionnel)", "all" : "TOUT", @@ -225,8 +225,8 @@ "settings_title" : "Réglages", "settings_nodes" : "Nœuds", "settings_current_node" : "Nœud actuel", - "settings_wallets" : "Portefeuilles", - "settings_display_balance" : "Afficher le solde", + "settings_wallets" : "Portefeuilles (Wallets)", + "settings_display_balance_as" : "Affichage du solde", "settings_currency" : "Devise", "settings_fee_priority" : "Priorité des frais", "settings_save_recipient_address" : "Sauvegarder l'adresse du bénéficiaire", @@ -252,8 +252,8 @@ "setup_successful" : "Votre code PIN a été configuré avec succès !", - "wallet_keys" : "Graine/Clefs du portefeuille", - "wallet_seed" : "Graine du portefeuille", + "wallet_keys" : "Phrase secrète (seed)/Clefs du portefeuille (wallet)", + "wallet_seed" : "Phrase secrète (seed) du portefeuille (wallet)", "private_key" : "Clef privée", "public_key" : "Clef publique", "view_key_private" : "Clef d'audit (view key) (privée)", @@ -295,21 +295,21 @@ "transaction_details_recipient_address" : "Adresse du bénéficiaire", - "wallet_list_title" : "Portefeuille Monero", - "wallet_list_create_new_wallet" : "Créer un Nouveau Portefeuille", - "wallet_list_restore_wallet" : "Restaurer un Portefeuille", - "wallet_list_load_wallet" : "Charger Portefeuille", - "wallet_list_loading_wallet" : "Chargement du portefeuille ${wallet_name}", - "wallet_list_failed_to_load" : "Échec de chargement du portefeuille ${wallet_name}. ${error}", - "wallet_list_removing_wallet" : "Suppression du portefeuille ${wallet_name}", - "wallet_list_failed_to_remove" : "Échec de la suppression du portefeuille ${wallet_name}. ${error}", + "wallet_list_title" : "Portefeuille (Wallet) Monero", + "wallet_list_create_new_wallet" : "Créer un Nouveau Portefeuille (Wallet)", + "wallet_list_restore_wallet" : "Restaurer un Portefeuille (Wallet)", + "wallet_list_load_wallet" : "Charger un Portefeuille (Wallet)", + "wallet_list_loading_wallet" : "Chargement du portefeuille (wallet) ${wallet_name}", + "wallet_list_failed_to_load" : "Échec de chargement du portefeuille (wallet) ${wallet_name}. ${error}", + "wallet_list_removing_wallet" : "Suppression du portefeuille (wallet) ${wallet_name}", + "wallet_list_failed_to_remove" : "Échec de la suppression du portefeuille (wallet) ${wallet_name}. ${error}", "widgets_address" : "Adresse", "widgets_restore_from_blockheight" : "Restaurer depuis une hauteur de bloc", "widgets_restore_from_date" : "Restaurer depuis une date", "widgets_or" : "ou", - "widgets_seed" : "Graine", + "widgets_seed" : "Phrase secrète (seed)", "router_no_route" : "Aucune route définie pour ${name}", @@ -317,7 +317,7 @@ "error_text_account_name" : "Le nom de compte ne peut contenir que des lettres et des chiffres\net sa longueur doit être comprise entre 1 et 15 caractères", "error_text_contact_name" : "Un nom de contact ne peut pas contenir les symboles ` , ' \"\net doit faire entre 1 et 32 caractères", - "error_text_address" : "L'adresse du portefeuille doit correspondre au type de\ncryptomonnaie", + "error_text_address" : "L'adresse du portefeuille (wallet) doit correspondre au type de\ncryptomonnaie", "error_text_node_address" : "Merci d'entrer une adresse IPv4", "error_text_node_port" : "Le port d'un nœud doit être un nombre compris entre 0 et 65535", "error_text_payment_id" : "Un ID de paiement ne peut être constitué que de 16 à 64 caractères hexadécimaux", @@ -325,8 +325,8 @@ "error_text_fiat" : "La valeur du montant ne peut dépasser le solde disponible.\nLa partie décimale doit comporter au plus 2 chiffres", "error_text_subaddress_name" : "Le nom de sous-adresse ne peut contenir les symboles ` , ' \"\net sa longueur doit être comprise entre 1 et 20 caractères", "error_text_amount" : "Le montant ne peut comporter que des nombres", - "error_text_wallet_name" : "Le nom du portefeuille ne peut contenir que des lettres, des chiffres, des symboles _ -\net sa longueur doit être comprise entre 1 et 33 caractères", - "error_text_keys" : "Les clefs du portefeuille ne peuvent être constituées que de 64 caractères hexadécimaux", + "error_text_wallet_name" : "Le nom du portefeuille (wallet) ne peut contenir que des lettres et des chiffres\net sa longueur doit être comprise entre 1 et 15 caractères", + "error_text_keys" : "Les clefs du portefeuille (wallet) ne peuvent être constituées que de 64 caractères hexadécimaux", "error_text_crypto_currency" : "La partie décimale\ndoit comporter au plus 12 chiffres", "error_text_minimal_limit" : "Échange pour ${provider} non créé. Le montant est inférieur au minimum : ${min} ${currency}", "error_text_maximum_limit" : "Échange pour ${provider} non créé. Le montant est supérieur au maximum : ${max} ${currency}", @@ -338,8 +338,8 @@ "auth_store_banned_for" : "Banni pour ", "auth_store_banned_minutes" : " minutes", "auth_store_incorrect_password" : "Mauvais code PIN", - "wallet_store_monero_wallet" : "Portefeuille Monero", - "wallet_restoration_store_incorrect_seed_length" : "Longueur de graine incorrecte", + "wallet_store_monero_wallet" : "Portefeuille (Wallet) Monero", + "wallet_restoration_store_incorrect_seed_length" : "Longueur de phrase secrète (seed) incorrecte", "full_balance" : "Solde Complet", @@ -385,10 +385,10 @@ "trade_state_finished" : "Terminé", "change_language" : "Changer de langue", - "change_language_to" : "Changer la langue vers ${language}?", + "change_language_to" : "Changer la langue vers ${language} ?", "paste" : "Coller", - "restore_from_seed_placeholder" : "Merci d'entrer ou de coller votre graine ici", + "restore_from_seed_placeholder" : "Merci d'entrer ou de coller votre phrase secrète (seed) ici", "add_new_word" : "Ajouter un nouveau mot", "incorrect_seed" : "Le texte entré est invalide.", @@ -407,26 +407,26 @@ "template" : "Modèle", "confirm_delete_template" : "Cette action va supprimer ce modèle. Souhaitez-vous continuer ?", - "confirm_delete_wallet" : "Cette action va supprimer ce portefeuille. Souhaitez-vous contnuer ?", + "confirm_delete_wallet" : "Cette action va supprimer ce portefeuille (wallet). Souhaitez-vous contnuer ?", "picker_description" : "Pour choisir ChangeNOW ou MorphToken, merci de modifier d'abord la paire de votre échange", - "change_wallet_alert_title" : "Changer le portefeuille actuel", - "change_wallet_alert_content" : "Souhaitez-vous changer le portefeuille actuel vers ${wallet_name}?", + "change_wallet_alert_title" : "Changer le portefeuille (wallet) actuel", + "change_wallet_alert_content" : "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?", - "creating_new_wallet" : "Création d'un nouveau portefeuille", + "creating_new_wallet" : "Création d'un nouveau portefeuille (wallet)", "creating_new_wallet_error" : "Erreur : ${description}", "seed_alert_title" : "Attention", - "seed_alert_content" : "La graine est la seule façon de restaurer votre portefeuille. L'avez-vous correctement écrit ?", + "seed_alert_content" : "La phrase secrète (seed) est la seule façon de restaurer votre portefeuille (wallet). L'avez-vous correctement écrite ?", "seed_alert_back" : "Retour", "seed_alert_yes" : "Oui, je suis sûr", - "exchange_sync_alert_content" : "Merci d'attendre que votre portefeuille soit synchronisé", + "exchange_sync_alert_content" : "Merci d'attendre que votre portefeuille (wallet) soit synchronisé", "pre_seed_title" : "IMPORTANT", - "pre_seed_description" : "Sur la page suivante vous allez voir une série de ${words} mots. Ils constituent votre graine unique et privé et sont le SEUL moyen de restaurer votre portefeuille en cas de perte ou de dysfonctionnement. Il est de VOTRE responsabilité d'écrire cette série de mots et de les stocker dans un lieu sûr en dehors de l'application Cake Wallet.", - "pre_seed_button_text" : "J'ai compris. Montrez moi ma graine", + "pre_seed_description" : "Sur la page suivante vous allez voir une série de ${words} mots. Ils constituent votre phrase secrète (seed) unique et privé et sont le SEUL moyen de restaurer votre portefeuille (wallet) en cas de perte ou de dysfonctionnement. Il est de VOTRE responsabilité d'écrire cette série de mots et de les stocker dans un lieu sûr en dehors de l'application Cake Wallet.", + "pre_seed_button_text" : "J'ai compris. Montrez moi ma phrase secrète (seed)", "xmr_to_error" : "Erreur XMR.TO", "xmr_to_error_description" : "Montant invalide. La partie décimale doit contenir au plus 8 chiffres", @@ -475,17 +475,17 @@ "xlm_extra_info" : "Merci de ne pas oublier de spécifier l'ID de mémo lors de l'envoi de la transaction XLM de l'échange", "xrp_extra_info" : "Merci de ne pas oublier de spécifier le tag de destination lors de l'envoi de la transaction XRP de l'échange", - "exchange_incorrect_current_wallet_for_xmr" : "Si vous souhaitez échanger des XMR du solde Monero de votre Cake Wallet, merci de sélectionner votre portefeuille Monero au préalable.", + "exchange_incorrect_current_wallet_for_xmr" : "Si vous souhaitez échanger des XMR du solde Monero de votre Cake Wallet, merci de sélectionner votre portefeuille (wallet) Monero au préalable.", "confirmed" : "Confirmé", "unconfirmed" : "Non confirmé", "displayable" : "Visible", "submit_request" : "soumettre une requête", - "buy_alert_content" : "Pour le moment nous ne supportons que l'achat de Bitcoin et Litecoin. Pour acheter du Bitcoin ou du Litecoin, merci de créer ou de sélectionner votre portefeuille Bitcoin ou Litecoin.", - "sell_alert_content": "Pour le moment nous ne supportons que la vente de Bitcoin. Pour vendre du Bitcoin, merci de créer ou de sélectionner votre portefeuille Bitcoint.", + "buy_alert_content" : "Pour le moment nous ne supportons que l'achat de Bitcoin et Litecoin. Pour acheter du Bitcoin ou du Litecoin, merci de créer ou de sélectionner votre portefeuille (wallet) Bitcoin ou Litecoin.", + "sell_alert_content": "Pour le moment nous ne supportons que la vente de Bitcoin. Pour vendre du Bitcoin, merci de créer ou de sélectionner votre portefeuille (wallet) Bitcoin.", - "outdated_electrum_wallet_description" : "Les nouveaux portefeuilles Bitcoin créés dans Cake ont dorénavant une graine de 24 mots. Il est impératif que vous créiez un nouveau portefeuille Bitcoin, que vous y transfériez tous vos fonds puis que vous cessiez d'utiliser le portefeuille avec une graine de 12 mots. Merci de faire cela immédiatement pour assurer la sécurité de vos avoirs.", + "outdated_electrum_wallet_description" : "Les nouveaux portefeuilles (wallets) Bitcoin créés dans Cake ont dorénavant une phrase secrète (seed) de 24 mots. Il est impératif que vous créiez un nouveau portefeuille Bitcoin, que vous y transfériez tous vos fonds puis que vous cessiez d'utiliser le portefeuille avec une phrase secrète de 12 mots. Merci de faire cela immédiatement pour assurer la sécurité de vos avoirs.", "understand" : "J'ai compris", "apk_update" : "Mise à jour d'APK", @@ -494,7 +494,7 @@ "buy_with" : "Acheter avec", "moonpay_alert_text" : "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", - "outdated_electrum_wallet_receive_warning": "Si ce portefeuille a une graine de 12 mots et a été créé dans Cake, NE PAS y déposer de Bitcoin. Tous les BTC transférés vers ce portefeuille seront perdus. Créez un nouveau portefeuille avec graine de 24 mots (appuyez sur le menu en haut à droite, sélectionnez Portefeuilles puis Créer un Nouveau Portefeuille et enfin Bitcoin) et transférez y IMMÉDIATEMENT vos BTC. Les nouveaux portefeuilles BTC Cake (avec graine de 24 mots) sont sécurisés", + "outdated_electrum_wallet_receive_warning": "Si ce portefeuille (wallet) a une phrase secrète (seed) de 12 mots et a été créé dans Cake, NE PAS y déposer de Bitcoin. Tous les BTC transférés vers ce portefeuille seront perdus. Créez un nouveau portefeuille avec phrase secrète de 24 mots (appuyez sur le menu en haut à droite, sélectionnez Portefeuilles puis Créer un Nouveau Portefeuille et enfin Bitcoin) et transférez y IMMÉDIATEMENT vos BTC. Les nouveaux portefeuilles BTC Cake (avec phrase secrète de 24 mots) sont sécurisés", "do_not_show_me": "Ne plus me montrer ceci à l'avenir", "unspent_coins_title" : "Pièces (coins) non dépensées", @@ -520,7 +520,7 @@ "yat_error" : "Erreur Yat", "yat_error_content" : "Aucune adresse associée à ce Yat. Essayez un autre Yat", "choose_address" : "\n\nMerci de choisir l'adresse :", - "yat_popup_title" : "L'adresse de votre portefeuille peut être emojifiée.", + "yat_popup_title" : "L'adresse de votre portefeuille (wallet) peut être emojifiée.", "yat_popup_content" : "Vous pouvez à présent envoyer et recevoir des cryptos dans Cake Wallet à l'aide de votre Yat - un nom d'utilisateur court à base d'emoji. Gérér les Yats à tout moment depuis l'écran de paramétrage", "second_intro_title" : "Une adresse emoji pour les gouverner tous", "second_intro_content" : "Votre Yat est une seule et unique adresse emoji qui remplace toutes vos longues adresses hexadécimales pour toutes vos cryptomonnaies.", From d137e570dfbd66397735184ab621c3ebf55d93cf Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:30:50 +0100 Subject: [PATCH 58/87] Update cake wallet version to 4.4.6 and monero.com to 1.1.0 (#503) --- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 1ba99ef0e..3a3d9ccf4 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.9" -MONERO_COM_BUILD_NUMBER=16 +MONERO_COM_VERSION="1.1.0" +MONERO_COM_BUILD_NUMBER=17 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.5" -CAKEWALLET_BUILD_NUMBER=113 +CAKEWALLET_VERSION="4.4.6" +CAKEWALLET_BUILD_NUMBER=116 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 4510b7213..40de6482a 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.9" -MONERO_COM_BUILD_NUMBER=19 +MONERO_COM_VERSION="1.1.0" +MONERO_COM_BUILD_NUMBER=20 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.5" -CAKEWALLET_BUILD_NUMBER=113 +CAKEWALLET_VERSION="4.4.6" +CAKEWALLET_BUILD_NUMBER=116 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From bc8900e879b41612a8f8a8a6771300e07223a514 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer Date: Wed, 14 Sep 2022 14:46:14 -0500 Subject: [PATCH 59/87] fix zaddr validation (#506) Sideshift only supports `zs` Sapling addresses now. They do not support `zc` Sprout addresses or `u` unified addresses --- lib/core/address_validator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index b9d6cf0d6..9ef8d3340 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -71,7 +71,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.hbar: return '[0-9a-zA-Z.]'; case CryptoCurrency.zaddr: - return '^zs[0-9a-zA-Z]{75}\&|^zc[0-9a-zA-Z]{93}\$'; + return '^zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; default: From e9807944c060b90604dd4559e09d273fed5b5531 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 14 Sep 2022 22:55:39 +0300 Subject: [PATCH 60/87] add tags and update titles for currencies (#505) * add tags and update titles for currencies AVAXC -> AVAX (C-CHAIN) BTTBSC -> BTT (BSC) USDTTRC20 -> USDT (TRX) USDCSOL -> USDC (SOL) * Update crypto_currency.dart Add ETH tag to usdc Co-authored-by: mkyq <53115730+mkyq@users.noreply.github.com> --- cw_core/lib/crypto_currency.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 8baca39a1..271066f58 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -80,18 +80,18 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28); static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29); - static const ape = CryptoCurrency(title: 'APE', iconPath: 'assets/images/ape_icon.png', raw: 30); - static const avaxc = CryptoCurrency(title: 'AVAXC', iconPath: 'assets/images/avaxc_icon.png', raw: 31); + static const ape = CryptoCurrency(title: 'APE', iconPath: 'assets/images/ape_icon.png', tag: 'ETH', raw: 30); + static const avaxc = CryptoCurrency(title: 'AVAX', iconPath: 'assets/images/avaxc_icon.png', tag: 'C-CHAIN', raw: 31); static const btt = CryptoCurrency(title: 'BTT', iconPath: 'assets/images/btt_icon.png', raw: 32); - static const bttbsc = CryptoCurrency(title: 'BTTBSC', iconPath: 'assets/images/bttbsc_icon.png', raw: 33); + static const bttbsc = CryptoCurrency(title: 'BTT', iconPath: 'assets/images/bttbsc_icon.png', tag: 'BSC', raw: 33); static const doge = CryptoCurrency(title: 'DOGE', iconPath: 'assets/images/doge_icon.png', raw: 34); static const firo = CryptoCurrency(title: 'FIRO', iconPath: 'assets/images/firo_icon.png', raw: 35); - static const usdttrc20 = CryptoCurrency(title: 'USDTTRC20', iconPath: 'assets/images/usdttrc20_icon.png', raw: 36); + static const usdttrc20 = CryptoCurrency(title: 'USDT', iconPath: 'assets/images/usdttrc20_icon.png', tag: 'TRX', raw: 36); static const hbar = CryptoCurrency(title: 'HBAR', iconPath: 'assets/images/hbar_icon.png', raw: 37); static const sc = CryptoCurrency(title: 'SC', iconPath: 'assets/images/sc_icon.png', raw: 38); static const sol = CryptoCurrency(title: 'SOL', iconPath: 'assets/images/sol_icon.png', raw: 39); - static const usdc = CryptoCurrency(title: 'USDC', iconPath: 'assets/images/usdc_icon.png', raw: 40); - static const usdcsol = CryptoCurrency(title: 'USDCSOL', iconPath: 'assets/images/usdcsol_icon.png', raw: 41); + static const usdc = CryptoCurrency(title: 'USDC', iconPath: 'assets/images/usdc_icon.png', tag: 'ETH', raw: 40); + static const usdcsol = CryptoCurrency(title: 'USDC', iconPath: 'assets/images/usdcsol_icon.png', tag: 'SOL', raw: 41); static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', name: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 42); static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', name: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 43); static const zen = CryptoCurrency(title: 'ZEN', iconPath: 'assets/images/zen_icon.png', raw: 44); @@ -300,4 +300,4 @@ class CryptoCurrency extends EnumerableItem with Serializable { @override String toString() => title; -} \ No newline at end of file +} From 3f931ed8f21aa83a4d43d929f42700738d6e1acc Mon Sep 17 00:00:00 2001 From: M Date: Wed, 14 Sep 2022 16:15:11 -0400 Subject: [PATCH 61/87] Update ios build number --- scripts/ios/app_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 40de6482a..eda8c87a7 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -19,7 +19,7 @@ MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.6" -CAKEWALLET_BUILD_NUMBER=116 +CAKEWALLET_BUILD_NUMBER=117 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From cb8cf8e77c8e238194380d0b0d97de6f4a8f3e71 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 14 Sep 2022 18:22:31 -0400 Subject: [PATCH 62/87] Temporary disable more options button on ionia gift card details screen. --- .../cards/ionia_gift_card_detail_page.dart | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart index 9132ad52a..a6326fc80 100644 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -133,19 +133,19 @@ class IoniaGiftCardDetailPage extends BasePage { if (!viewModel.giftCard.isEmpty) { return Column( children: [ - PrimaryButton( - onPressed: () async { - final amount = await Navigator.of(context) - .pushNamed(Routes.ioniaMoreOptionsPage, arguments: [viewModel.giftCard]) as String; - if (amount != null) { - viewModel.updateRemaining(double.parse(amount)); - } - }, - text: S.of(context).more_options, - color: Theme.of(context).accentTextTheme.caption.color, - textColor: Theme.of(context).primaryTextTheme.title.color, - ), - SizedBox(height: 12), + //PrimaryButton( + // onPressed: () async { + // final amount = await Navigator.of(context) + // .pushNamed(Routes.ioniaMoreOptionsPage, arguments: [viewModel.giftCard]) as String; + // if (amount != null) { + // viewModel.updateRemaining(double.parse(amount)); + // } + // }, + // text: S.of(context).more_options, + // color: Theme.of(context).accentTextTheme.caption.color, + // textColor: Theme.of(context).primaryTextTheme.title.color, + //), + //SizedBox(height: 12), LoadingPrimaryButton( isLoading: viewModel.redeemState is IsExecutingState, onPressed: () => viewModel.redeem().then( From b978132fcf8703f78986038e0e6f731a62ab64ba Mon Sep 17 00:00:00 2001 From: M Date: Wed, 14 Sep 2022 19:04:46 -0400 Subject: [PATCH 63/87] Update ios build number for monero.com --- scripts/ios/app_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index eda8c87a7..5d99e66c2 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,7 +14,7 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.1.0" -MONERO_COM_BUILD_NUMBER=20 +MONERO_COM_BUILD_NUMBER=21 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" From 04e059a3de2413f24e70c5898e62de55add0ef2c Mon Sep 17 00:00:00 2001 From: M Date: Thu, 15 Sep 2022 08:23:42 -0400 Subject: [PATCH 64/87] Update build number for android --- scripts/android/app_env.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 3a3d9ccf4..bcb8ef231 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,13 +15,13 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.1.0" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_BUILD_NUMBER=19 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.4.6" -CAKEWALLET_BUILD_NUMBER=116 +CAKEWALLET_BUILD_NUMBER=118 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" From 1a9becbebcf46a7f95e13afddd0454a89dffc9b7 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:28:43 +0300 Subject: [PATCH 65/87] fix color for login back button (#510) --- lib/src/screens/ionia/auth/ionia_login_page.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/ionia/auth/ionia_login_page.dart index bcbc0fee3..44df933b5 100644 --- a/lib/src/screens/ionia/auth/ionia_login_page.dart +++ b/lib/src/screens/ionia/auth/ionia_login_page.dart @@ -26,9 +26,6 @@ class IoniaLoginPage extends BasePage { final IoniaAuthViewModel _authViewModel; - @override - Color get titleColor => Colors.black; - final TextEditingController _emailController; @override From 4f0b7bf51ac8b527272b004c46726d58a9fb0869 Mon Sep 17 00:00:00 2001 From: Mathias Herberts Date: Fri, 16 Sep 2022 19:22:51 +0200 Subject: [PATCH 66/87] Minor fixes for French locale post v4.4.6 (#508) * Fixed typo and added missing settings_display_balance entry. * Fixed typographical errors and typo --- res/values/strings_fr.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 7d5a3c74d..b347086ae 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -56,7 +56,7 @@ "reconnect" : "Reconnecter", "wallets" : "Portefeuilles (Wallets)", "show_seed" : "Visualiser la phrase secrète (seed)", - "show_keys" : "Visualiser les /clefs", + "show_keys" : "Visualiser la phrase secrète (seed) et les clefs", "address_book_menu" : "Carnet d'Adresses", "reconnection" : "Reconnexion", "reconnect_alert_text" : "Êtes vous certain de vouloir vous reconnecter ?", @@ -226,7 +226,7 @@ "settings_nodes" : "Nœuds", "settings_current_node" : "Nœud actuel", "settings_wallets" : "Portefeuilles (Wallets)", - "settings_display_balance_as" : "Affichage du solde", + "settings_display_balance" : "Affichage du solde", "settings_currency" : "Devise", "settings_fee_priority" : "Priorité des frais", "settings_save_recipient_address" : "Sauvegarder l'adresse du bénéficiaire", @@ -541,7 +541,7 @@ "already_have_account": "Vous avez déjà un compte ?", "create_account": "Créer un compte", "privacy_policy": "Politique de confidentialité", - "welcome_to_cakepay": "Bienvenue sur Cake Pay!", + "welcome_to_cakepay": "Bienvenue sur Cake Pay !", "sign_up": "S'inscrire", "forgot_password": "Mot de passe oublié", "reset_password": "Réinitialiser le mot de passe", @@ -555,7 +555,7 @@ "dont_get_code": "Vous ne recevez pas le code ?", "resend_code": "Veuillez le renvoyer", "debit_card": "Carte de débit", - "cakepay_prepaid_card": "Carte de débit prépayée CakePay", + "cakepay_prepaid_card": "Carte de débit prépayée Cake Pay", "no_id_needed": "Aucune pièce d'identité nécessaire !", "frequently_asked_questions": "Foire aux questions", "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille numérique sont soumis aux conditions générales de l'accord du titulaire de carte applicable avec l'émetteur de la carte de paiement, en vigueur à partir de de temps en temps.", @@ -631,7 +631,7 @@ "open_gift_card": "Ouvrir la carte-cadeau", "contact_support": "Contacter l'assistance", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", - "introducing_cake_pay": "Présentation de Cake Pay!", + "introducing_cake_pay": "Présentation de Cake Pay !", "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "automatic": "Automatique", "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", From 54bc40c50307a8e13504ea6114e76353ca810251 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 16 Sep 2022 23:15:34 +0300 Subject: [PATCH 67/87] hide tips section (#509) --- lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart index 5b0109d0e..9642be130 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -155,6 +155,7 @@ class IoniaBuyGiftCardDetailPage extends BasePage { ], ), ), + if(ioniaPurchaseViewModel.ioniaMerchant.acceptsTips) Padding( padding: const EdgeInsets.fromLTRB(24.0, 24.0, 0, 24.0), child: Column( From c921ad890a00bd700f96716eb01f39a09d20b266 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Mon, 19 Sep 2022 18:09:23 +0300 Subject: [PATCH 68/87] Truncate to double (#511) * Truncate to double * fix format remaining amount --- lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart | 2 +- lib/view_model/ionia/ionia_custom_redeem_view_model.dart | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart b/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart index 4bb76848f..88d8f4e97 100644 --- a/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart +++ b/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart @@ -117,7 +117,7 @@ class IoniaCustomRedeemPage extends BasePage { Observer(builder: (_)=> !ioniaCustomRedeemViewModel.disableRedeem ? Center( - child: Text('\$${giftCard.remainingAmount} - \$${ioniaCustomRedeemViewModel.amount} = \$${ioniaCustomRedeemViewModel.remaining} ${S.of(context).remaining}', + child: Text('\$${giftCard.remainingAmount} - \$${ioniaCustomRedeemViewModel.amount} = \$${ioniaCustomRedeemViewModel.formattedRemaining} ${S.of(context).remaining}', style: TextStyle( color: Theme.of(context).primaryTextTheme.headline.color, ),), diff --git a/lib/view_model/ionia/ionia_custom_redeem_view_model.dart b/lib/view_model/ionia/ionia_custom_redeem_view_model.dart index 88cc08e83..6bd8e15fa 100644 --- a/lib/view_model/ionia/ionia_custom_redeem_view_model.dart +++ b/lib/view_model/ionia/ionia_custom_redeem_view_model.dart @@ -16,6 +16,9 @@ abstract class IoniaCustomRedeemViewModelBase with Store { @computed double get remaining => amount <= giftCard.remainingAmount ? giftCard.remainingAmount - amount : 0; + @computed + String get formattedRemaining => remaining.toStringAsFixed(2); + @computed bool get disableRedeem => amount > giftCard.remainingAmount; From 6b8e749abfd1e4d642d9aad82847816306e12950 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 26 Sep 2022 23:12:32 +0200 Subject: [PATCH 69/87] Fix SimpleSwap create exchange (#522) * Check for status code 201 instead of 200 to follow SimpleSwap API documentation * allow 200 and 201 success status code --- lib/exchange/simpleswap/simpleswap_exchange_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart index cf6fb3d38..1183de696 100644 --- a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart +++ b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart @@ -91,7 +91,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { final response = await post(uri, headers: headers, body: json.encode(body)); - if (response.statusCode != 200) { + if (response.statusCode != 200 && response.statusCode != 201) { if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['message'] as String; From 40e4f7de1fc254fbfbad4fbfa2f13e50512524b2 Mon Sep 17 00:00:00 2001 From: M Date: Mon, 26 Sep 2022 17:15:51 -0400 Subject: [PATCH 70/87] Changed app version to 4.4.7 (1.1.1 for monero.com) --- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index bcb8ef231..311913329 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.1.0" -MONERO_COM_BUILD_NUMBER=19 +MONERO_COM_VERSION="1.1.1" +MONERO_COM_BUILD_NUMBER=20 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.6" -CAKEWALLET_BUILD_NUMBER=118 +CAKEWALLET_VERSION="4.4.7" +CAKEWALLET_BUILD_NUMBER=119 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 5d99e66c2..236482b05 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.1.0" -MONERO_COM_BUILD_NUMBER=21 +MONERO_COM_VERSION="1.1.1" +MONERO_COM_BUILD_NUMBER=22 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.6" -CAKEWALLET_BUILD_NUMBER=117 +CAKEWALLET_VERSION="4.4.7" +CAKEWALLET_BUILD_NUMBER=118 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" From 1beb18b04575d2e0735ec52f2592f0bb330800db Mon Sep 17 00:00:00 2001 From: M Date: Wed, 12 Oct 2022 13:09:57 -0400 Subject: [PATCH 71/87] Flutter upgrade --- analysis_options.yaml | 114 +++++---- cw_bitcoin/lib/address_from_output.dart | 6 +- cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- cw_bitcoin/lib/bitcoin_amount_format.dart | 4 +- cw_bitcoin/lib/bitcoin_mnemonic.dart | 13 +- .../lib/bitcoin_transaction_credentials.dart | 6 +- .../lib/bitcoin_transaction_priority.dart | 12 +- cw_bitcoin/lib/bitcoin_wallet.dart | 57 +++-- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 14 +- .../bitcoin_wallet_creation_credentials.dart | 6 +- cw_bitcoin/lib/bitcoin_wallet_keys.dart | 4 +- cw_bitcoin/lib/bitcoin_wallet_service.dart | 18 +- cw_bitcoin/lib/electrum.dart | 111 +++++---- cw_bitcoin/lib/electrum_balance.dart | 8 +- .../lib/electrum_transaction_history.dart | 18 +- cw_bitcoin/lib/electrum_transaction_info.dart | 73 +++--- cw_bitcoin/lib/electrum_wallet.dart | 104 ++++---- cw_bitcoin/lib/electrum_wallet_addresses.dart | 29 ++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 60 +++-- cw_bitcoin/lib/file.dart | 15 +- cw_bitcoin/lib/litecoin_wallet.dart | 56 +++-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 14 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 18 +- .../lib/pending_bitcoin_transaction.dart | 7 +- cw_bitcoin/lib/script_hash.dart | 3 +- cw_bitcoin/lib/utils.dart | 42 ++-- cw_bitcoin/pubspec.lock | 216 ++++++++--------- cw_bitcoin/pubspec.yaml | 28 +-- cw_core/lib/account.dart | 4 +- cw_core/lib/account_list.dart | 4 +- cw_core/lib/crypto_amount_format.dart | 2 +- cw_core/lib/crypto_currency.dart | 22 +- cw_core/lib/currency_for_wallet_type.dart | 2 +- cw_core/lib/enumerable_item.dart | 4 +- cw_core/lib/get_height_by_date.dart | 4 +- cw_core/lib/key.dart | 4 +- cw_core/lib/monero_amount_format.dart | 6 +- cw_core/lib/monero_balance.dart | 7 +- cw_core/lib/monero_transaction_priority.dart | 6 +- cw_core/lib/monero_wallet_keys.dart | 8 +- cw_core/lib/node.dart | 37 +-- cw_core/lib/output_info.dart | 26 +- cw_core/lib/pathForWallet.dart | 6 +- cw_core/lib/sec_random_native.dart | 2 +- cw_core/lib/subaddress.dart | 4 +- cw_core/lib/transaction_direction.dart | 18 +- cw_core/lib/transaction_history.dart | 5 +- cw_core/lib/transaction_info.dart | 20 +- cw_core/lib/transaction_priority.dart | 2 +- cw_core/lib/unspent_coins_info.dart | 10 +- cw_core/lib/wallet_addresses.dart | 9 +- .../lib/wallet_addresses_with_account.dart | 4 +- cw_core/lib/wallet_base.dart | 10 +- cw_core/lib/wallet_credentials.dart | 12 +- cw_core/lib/wallet_info.dart | 30 +-- cw_core/lib/wallet_type.dart | 4 +- cw_core/pubspec.lock | 181 +++++++------- cw_core/pubspec.yaml | 22 +- cw_haven/lib/api/account_list.dart | 16 +- cw_haven/lib/api/convert_utf8_to_string.dart | 6 +- cw_haven/lib/api/cw_haven.dart | 2 +- .../connection_to_node_exception.dart | 2 +- .../creation_transaction_exception.dart | 2 +- .../exceptions/setup_wallet_exception.dart | 2 +- .../exceptions/wallet_creation_exception.dart | 2 +- .../exceptions/wallet_opening_exception.dart | 2 +- .../wallet_restore_from_keys_exception.dart | 2 +- .../wallet_restore_from_seed_exception.dart | 2 +- cw_haven/lib/api/monero_output.dart | 2 +- cw_haven/lib/api/signatures.dart | 4 +- cw_haven/lib/api/structs/account_row.dart | 7 +- .../lib/api/structs/haven_balance_row.dart | 7 +- cw_haven/lib/api/structs/haven_rate.dart | 7 +- .../lib/api/structs/pending_transaction.dart | 14 +- cw_haven/lib/api/structs/subaddress_row.dart | 12 +- .../lib/api/structs/transaction_info_row.dart | 30 +-- cw_haven/lib/api/structs/ut8_box.dart | 4 +- cw_haven/lib/api/subaddress_list.dart | 18 +- cw_haven/lib/api/transaction_history.dart | 98 ++++---- cw_haven/lib/api/types.dart | 4 +- cw_haven/lib/api/wallet.dart | 71 +++--- cw_haven/lib/api/wallet_manager.dart | 124 +++++----- cw_haven/lib/haven_account_list.dart | 4 +- cw_haven/lib/haven_balance.dart | 2 +- cw_haven/lib/haven_subaddress_list.dart | 17 +- ...aven_transaction_creation_credentials.dart | 5 +- cw_haven/lib/haven_transaction_info.dart | 15 +- cw_haven/lib/haven_wallet.dart | 56 +++-- cw_haven/lib/haven_wallet_addresses.dart | 25 +- cw_haven/lib/haven_wallet_service.dart | 43 ++-- cw_haven/lib/pending_haven_transaction.dart | 14 +- cw_haven/lib/update_haven_rate.dart | 2 +- cw_haven/pubspec.lock | 181 +++++++------- cw_haven/pubspec.yaml | 20 +- cw_monero/lib/api/account_list.dart | 16 +- cw_monero/lib/api/convert_utf8_to_string.dart | 6 +- .../connection_to_node_exception.dart | 2 +- .../creation_transaction_exception.dart | 2 +- .../exceptions/setup_wallet_exception.dart | 2 +- .../exceptions/wallet_creation_exception.dart | 2 +- .../exceptions/wallet_opening_exception.dart | 2 +- .../wallet_restore_from_keys_exception.dart | 2 +- .../wallet_restore_from_seed_exception.dart | 2 +- cw_monero/lib/api/monero_output.dart | 4 +- cw_monero/lib/api/signatures.dart | 4 +- cw_monero/lib/api/structs/account_row.dart | 7 +- .../lib/api/structs/pending_transaction.dart | 28 +-- cw_monero/lib/api/structs/subaddress_row.dart | 12 +- .../lib/api/structs/transaction_info_row.dart | 26 +- cw_monero/lib/api/structs/ut8_box.dart | 4 +- cw_monero/lib/api/subaddress_list.dart | 20 +- cw_monero/lib/api/transaction_history.dart | 82 +++---- cw_monero/lib/api/types.dart | 4 +- cw_monero/lib/api/wallet.dart | 76 +++--- cw_monero/lib/api/wallet_manager.dart | 130 +++++----- cw_monero/lib/monero_account_list.dart | 5 +- cw_monero/lib/monero_subaddress_list.dart | 19 +- ...nero_transaction_creation_credentials.dart | 2 +- cw_monero/lib/monero_transaction_info.dart | 13 +- cw_monero/lib/monero_wallet.dart | 79 +++--- cw_monero/lib/monero_wallet_addresses.dart | 27 ++- cw_monero/lib/monero_wallet_service.dart | 37 ++- cw_monero/lib/pending_monero_transaction.dart | 14 +- cw_monero/pubspec.lock | 227 +++++++++++------- cw_monero/pubspec.yaml | 22 +- ios/Podfile.lock | 84 +++---- ios/Runner.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/anypay/any_pay_payment.dart | 24 +- .../any_pay_payment_committed_info.dart | 10 +- lib/anypay/any_pay_payment_instruction.dart | 10 +- lib/anypay/any_pay_trasnaction.dart | 6 +- lib/anypay/anypay_api.dart | 24 +- lib/bitcoin/cw_bitcoin.dart | 23 +- lib/buy/buy_amount.dart | 6 +- lib/buy/buy_exception.dart | 2 +- lib/buy/buy_provider.dart | 2 +- lib/buy/buy_provider_description.dart | 6 +- lib/buy/get_buy_provider_icon.dart | 2 +- lib/buy/moonpay/moonpay_buy_provider.dart | 21 +- lib/buy/order.dart | 35 +-- lib/buy/wyre/wyre_buy_provider.dart | 23 +- lib/core/address_label_validator.dart | 2 +- lib/core/address_validator.dart | 4 +- lib/core/amount.dart | 54 ++--- lib/core/amount_converter.dart | 8 +- lib/core/amount_validator.dart | 2 +- lib/core/auth_service.dart | 8 +- lib/core/auth_state.dart | 4 +- lib/core/backup_service.dart | 75 +++--- lib/core/fiat_conversion_service.dart | 2 +- lib/core/key_service.dart | 7 +- lib/core/monero_account_label_validator.dart | 2 +- lib/core/seed_validator.dart | 15 +- lib/core/sync_status_title.dart | 2 + lib/core/validator.dart | 24 +- lib/core/wallet_creation_service.dart | 24 +- lib/core/wallet_creation_state.dart | 2 +- lib/core/wallet_loading_service.dart | 15 +- lib/di.dart | 114 +++++---- lib/entities/action_list_display_mode.dart | 2 +- lib/entities/balance_display_mode.dart | 6 +- lib/entities/biometric_auth.dart | 5 +- lib/entities/calculate_fiat_amount.dart | 2 +- lib/entities/calculate_fiat_amount_raw.dart | 2 +- lib/entities/contact.dart | 11 +- lib/entities/contact_base.dart | 2 + lib/entities/contact_record.dart | 5 +- lib/entities/default_settings_migration.dart | 128 +++++----- lib/entities/encrypt.dart | 24 +- lib/entities/fiat_currency.dart | 4 +- lib/entities/fio_address_provider.dart | 2 +- lib/entities/fs_migration.dart | 79 ++++-- lib/entities/get_encryption_key.dart | 2 +- lib/entities/ios_legacy_helper.dart | 20 +- lib/entities/language_service.dart | 2 +- lib/entities/load_current_wallet.dart | 5 + lib/entities/mnemonic_item.dart | 2 +- lib/entities/node_list.dart | 68 +++--- lib/entities/openalias_record.dart | 11 +- lib/entities/parse_address_from_domain.dart | 2 +- lib/entities/parsed_address.dart | 33 ++- lib/entities/qr_scanner.dart | 20 +- lib/entities/record.dart | 2 +- lib/entities/secret_store_key.dart | 2 +- lib/entities/template.dart | 9 +- lib/entities/transaction_description.dart | 6 +- lib/entities/transaction_history.dart | 3 + lib/entities/unstoppable_domain_address.dart | 4 +- lib/entities/update_haven_rate.dart | 29 ++- lib/entities/wallet_contact.dart | 1 + lib/entities/wallet_description.dart | 2 +- lib/entities/yat_record.dart | 17 +- .../changenow_exchange_provider.dart | 29 +-- lib/exchange/changenow/changenow_request.dart | 14 +- lib/exchange/exchange_pair.dart | 5 +- lib/exchange/exchange_provider.dart | 22 +- .../exchange_provider_description.dart | 10 +- lib/exchange/exchange_template.dart | 12 +- lib/exchange/exchange_trade_state.dart | 4 +- lib/exchange/limits.dart | 4 +- lib/exchange/limits_state.dart | 4 +- .../morphtoken_exchange_provider.dart | 41 ++-- .../morphtoken/morphtoken_request.dart | 10 +- .../sideshift_exchange_provider.dart | 46 ++-- lib/exchange/sideshift/sideshift_request.dart | 14 +- .../simpleswap_exchange_provider.dart | 25 +- .../simpleswap/simpleswap_request.dart | 11 +- lib/exchange/trade.dart | 57 +++-- lib/exchange/trade_not_found_exeption.dart | 6 +- lib/exchange/trade_state.dart | 6 +- .../xmrto/xmrto_exchange_provider.dart | 41 ++-- lib/exchange/xmrto/xmrto_trade_request.dart | 14 +- lib/haven/cw_haven.dart | 95 +++++--- lib/ionia/ionia_anypay.dart | 16 +- lib/ionia/ionia_api.dart | 179 +++++++------- lib/ionia/ionia_category.dart | 6 +- lib/ionia/ionia_create_state.dart | 10 +- lib/ionia/ionia_gift_card.dart | 38 +-- lib/ionia/ionia_gift_card_instruction.dart | 2 +- lib/ionia/ionia_merchant.dart | 164 +++---------- lib/ionia/ionia_order.dart | 10 +- lib/ionia/ionia_service.dart | 65 +++-- lib/ionia/ionia_tip.dart | 7 +- lib/ionia/ionia_token_data.dart | 2 +- lib/ionia/ionia_virtual_card.dart | 24 +- lib/main.dart | 38 +-- lib/monero/cw_monero.dart | 69 +++--- lib/reactions/check_connection.dart | 2 +- lib/reactions/fiat_rate_update.dart | 14 +- .../on_authentication_state_change.dart | 9 +- lib/reactions/on_current_fiat_change.dart | 12 +- lib/reactions/on_current_node_change.dart | 6 +- lib/reactions/on_current_wallet_change.dart | 23 +- .../on_wallet_sync_status_change.dart | 4 +- lib/router.dart | 16 +- lib/src/screens/auth/auth_page.dart | 70 +++--- lib/src/screens/backup/backup_page.dart | 16 +- .../backup/edit_backup_password_page.dart | 4 +- lib/src/screens/base_page.dart | 44 ++-- lib/src/screens/buy/buy_webview_page.dart | 35 +-- lib/src/screens/buy/pre_order_page.dart | 44 ++-- .../screens/buy/widgets/buy_list_item.dart | 22 +- .../screens/contact/contact_list_page.dart | 89 +++---- lib/src/screens/contact/contact_page.dart | 14 +- lib/src/screens/dashboard/dashboard_page.dart | 77 +++--- .../screens/dashboard/wallet_menu_item.dart | 6 +- .../dashboard/widgets/action_button.dart | 19 +- .../dashboard/widgets/address_page.dart | 42 ++-- .../dashboard/widgets/balance_page.dart | 66 ++--- .../dashboard/widgets/date_section_raw.dart | 4 +- .../dashboard/widgets/filter_tile.dart | 2 +- .../dashboard/widgets/filter_widget.dart | 46 ++-- .../screens/dashboard/widgets/header_row.dart | 8 +- .../dashboard/widgets/market_place_page.dart | 4 +- .../dashboard/widgets/menu_widget.dart | 68 +++--- .../screens/dashboard/widgets/order_row.dart | 28 +-- .../dashboard/widgets/sync_indicator.dart | 8 +- .../widgets/sync_indicator_icon.dart | 4 +- .../screens/dashboard/widgets/trade_row.dart | 45 ++-- .../dashboard/widgets/transaction_raw.dart | 34 +-- .../dashboard/widgets/transactions_page.dart | 14 +- .../screens/disclaimer/disclaimer_page.dart | 52 ++-- lib/src/screens/exchange/exchange_page.dart | 145 ++++++----- .../exchange/exchange_template_page.dart | 104 ++++---- .../exchange/widgets/currency_picker.dart | 27 +-- .../widgets/currency_picker_item_widget.dart | 25 +- .../widgets/currency_picker_widget.dart | 10 +- .../exchange/widgets/exchange_card.dart | 142 +++++------ .../screens/exchange/widgets/picker_item.dart | 5 +- .../widgets/present_provider_picker.dart | 11 +- .../exchange_trade/exchange_confirm_page.dart | 30 +-- .../exchange_trade/exchange_trade_item.dart | 6 +- .../exchange_trade/exchange_trade_page.dart | 73 +++--- .../exchange_trade/information_page.dart | 10 +- .../exchange_trade/widgets/timer_widget.dart | 10 +- lib/src/screens/faq/faq_item.dart | 9 +- .../ionia/auth/ionia_create_account_page.dart | 10 +- .../screens/ionia/auth/ionia_login_page.dart | 6 +- .../ionia/auth/ionia_verify_otp_page.dart | 6 +- .../ionia/auth/ionia_welcome_page.dart | 10 +- .../ionia/cards/ionia_account_cards_page.dart | 36 +-- .../ionia/cards/ionia_account_page.dart | 23 +- .../cards/ionia_activate_debit_card_page.dart | 4 +- .../cards/ionia_buy_card_detail_page.dart | 81 ++++--- .../ionia/cards/ionia_buy_gift_card.dart | 22 +- .../ionia/cards/ionia_custom_redeem_page.dart | 18 +- .../ionia/cards/ionia_custom_tip_page.dart | 18 +- .../ionia/cards/ionia_debit_card_page.dart | 58 ++--- .../cards/ionia_gift_card_detail_page.dart | 33 ++- .../ionia/cards/ionia_manage_cards_page.dart | 61 ++--- .../ionia/cards/ionia_more_options_page.dart | 15 +- .../cards/ionia_payment_status_page.dart | 28 +-- lib/src/screens/ionia/widgets/card_item.dart | 26 +- .../screens/ionia/widgets/confirm_modal.dart | 35 +-- .../ionia/widgets/ionia_alert_model.dart | 14 +- .../ionia/widgets/ionia_filter_modal.dart | 10 +- lib/src/screens/ionia/widgets/ionia_tile.dart | 14 +- .../ionia/widgets/rounded_checkbox.dart | 4 +- .../ionia/widgets/text_icon_button.dart | 13 +- .../monero_account_edit_or_create_page.dart | 6 +- .../monero_account_list_page.dart | 13 +- .../monero_accounts/widgets/account_tile.dart | 41 ++-- .../screens/new_wallet/new_wallet_page.dart | 46 ++-- .../new_wallet/new_wallet_type_page.dart | 69 ++---- .../new_wallet/widgets/select_button.dart | 18 +- .../nodes/node_create_or_edit_page.dart | 6 +- lib/src/screens/nodes/nodes_list_page.dart | 73 +++--- .../screens/nodes/widgets/node_list_row.dart | 12 +- .../order_details/order_details_page.dart | 1 + lib/src/screens/pin_code/pin_code_widget.dart | 90 ++++--- .../screens/receive/fullscreen_qr_page.dart | 17 +- lib/src/screens/receive/receive_page.dart | 65 ++--- .../screens/receive/widgets/address_cell.dart | 67 +++--- .../screens/receive/widgets/header_tile.dart | 12 +- lib/src/screens/receive/widgets/qr_image.dart | 6 +- .../screens/receive/widgets/qr_painter.dart | 8 +- .../screens/receive/widgets/qr_widget.dart | 29 +-- lib/src/screens/rescan/rescan_page.dart | 4 +- .../restore/restore_from_backup_page.dart | 4 +- .../restore_wallet_from_keys_page.dart | 6 +- .../restore_wallet_from_seed_details.dart | 12 +- .../restore_wallet_from_seed_page.dart | 25 +- .../restore/restore_wallet_options_page.dart | 6 +- .../wallet_restore_from_keys_form.dart | 13 +- .../wallet_restore_from_seed_form.dart | 24 +- .../screens/restore/wallet_restore_page.dart | 78 +++--- .../restore/widgets/restore_button.dart | 14 +- lib/src/screens/root/root.dart | 17 +- lib/src/screens/seed/pre_seed_page.dart | 12 +- lib/src/screens/seed/wallet_seed_page.dart | 43 ++-- .../seed_language/seed_language_page.dart | 8 +- .../widgets/seed_language_picker.dart | 24 +- lib/src/screens/send/send_page.dart | 76 +++--- lib/src/screens/send/send_template_page.dart | 64 ++--- .../widgets/choose_yat_address_alert.dart | 10 +- .../send/widgets/confirm_sending_alert.dart | 81 +++---- .../widgets/extract_address_from_parsed.dart | 6 +- .../widgets/prefix_currency_icon_widget.dart | 4 +- lib/src/screens/send/widgets/send_card.dart | 128 +++++----- .../screens/settings/items/settings_item.dart | 14 +- lib/src/screens/settings/settings.dart | 4 +- .../widgets/settings_cell_with_arrow.dart | 4 +- .../widgets/settings_choices_cell.dart | 10 +- .../widgets/settings_link_provider_cell.dart | 19 +- .../widgets/settings_picker_cell.dart | 24 +- .../widgets/settings_switcher_cell.dart | 6 +- .../widgets/settings_version_cell.dart | 4 +- .../setup_pin_code/setup_pin_code.dart | 7 +- .../address_edit_or_create_page.dart | 6 +- lib/src/screens/support/support_page.dart | 6 +- .../trade_details/track_trade_list_item.dart | 5 +- .../trade_details_list_card.dart | 24 +- .../trade_details/trade_details_page.dart | 2 + .../trade_details_status_item.dart | 2 +- .../blockexplorer_list_item.dart | 3 +- .../standart_list_item.dart | 2 +- .../textfield_list_item.dart | 5 +- .../transaction_details_list_item.dart | 2 +- .../transaction_details_page.dart | 6 +- .../widgets/textfield_list_row.dart | 23 +- .../unspent_coins_details_page.dart | 6 +- .../unspent_coins_list_page.dart | 4 +- .../widgets/unspent_coins_list_item.dart | 20 +- .../widgets/unspent_coins_switch_row.dart | 11 +- .../screens/wallet_keys/wallet_keys_page.dart | 2 +- .../screens/wallet_list/wallet_list_page.dart | 67 +++--- lib/src/screens/wallet_list/wallet_menu.dart | 2 +- .../screens/wallet_list/wallet_menu_item.dart | 8 +- .../widgets/wallet_menu_alert.dart | 12 +- lib/src/screens/welcome/welcome_page.dart | 82 +++---- .../yat/widgets/first_introduction.dart | 4 +- .../yat/widgets/second_introduction.dart | 4 +- .../yat/widgets/third_introduction.dart | 7 +- lib/src/screens/yat/widgets/yat_bar.dart | 2 +- .../screens/yat/widgets/yat_close_button.dart | 2 +- .../yat/widgets/yat_page_indicator.dart | 2 +- lib/src/screens/yat/yat_popup.dart | 2 +- lib/src/screens/yat_emoji_id.dart | 8 +- lib/src/widgets/address_text_field.dart | 105 ++++---- lib/src/widgets/alert_background.dart | 2 +- lib/src/widgets/alert_close_button.dart | 2 +- lib/src/widgets/alert_with_one_action.dart | 21 +- lib/src/widgets/alert_with_two_actions.dart | 28 +-- lib/src/widgets/base_alert_dialog.dart | 28 ++- lib/src/widgets/base_text_form_field.dart | 56 ++--- lib/src/widgets/blockchain_height_widget.dart | 18 +- lib/src/widgets/cake_scrollbar.dart | 14 +- lib/src/widgets/check_box_picker.dart | 29 +-- lib/src/widgets/checkbox_widget.dart | 12 +- .../widgets/collapsible_standart_list.dart | 49 ++-- lib/src/widgets/discount_badge.dart | 8 +- lib/src/widgets/introducing_card.dart | 19 +- lib/src/widgets/market_place_item.dart | 20 +- lib/src/widgets/nav_bar.dart | 28 +-- lib/src/widgets/picker.dart | 51 ++-- lib/src/widgets/primary_button.dart | 111 +++++---- .../scollable_with_bottom_section.dart | 8 +- lib/src/widgets/seed_language_selector.dart | 4 +- lib/src/widgets/seed_widget.dart | 26 +- lib/src/widgets/standard_checkbox.dart | 19 +- lib/src/widgets/standard_list.dart | 57 ++--- lib/src/widgets/standart_list_card.dart | 11 +- lib/src/widgets/standart_list_row.dart | 14 +- lib/src/widgets/standart_list_status_row.dart | 12 +- lib/src/widgets/standart_switch.dart | 6 +- lib/src/widgets/template_tile.dart | 16 +- lib/src/widgets/trail_button.dart | 14 +- .../validable_annotated_editable_text.dart | 38 +-- lib/store/app_store.dart | 10 +- lib/store/dashboard/orders_store.dart | 16 +- lib/store/dashboard/trade_filter_store.dart | 2 +- lib/store/dashboard/trades_store.dart | 23 +- .../dashboard/transaction_filter_store.dart | 10 +- lib/store/node_list_store.dart | 14 +- lib/store/secret_store.dart | 5 +- lib/store/settings_store.dart | 206 +++++++++------- .../templates/exchange_template_store.dart | 14 +- lib/store/templates/send_template_store.dart | 25 +- lib/store/yat/yat_exception.dart | 2 +- lib/store/yat/yat_store.dart | 31 ++- lib/themes/bright_theme.dart | 92 ++++--- lib/themes/dark_theme.dart | 92 ++++--- lib/themes/light_theme.dart | 92 ++++--- lib/themes/theme_base.dart | 2 +- lib/themes/theme_list.dart | 4 +- lib/typography.dart | 38 +-- lib/utils/date_picker.dart | 16 +- lib/utils/debounce.dart | 2 +- lib/utils/item_cell.dart | 4 +- lib/utils/list_section.dart | 2 +- lib/utils/mobx.dart | 20 +- lib/utils/show_bar.dart | 95 ++++---- lib/utils/show_pop_up.dart | 10 +- lib/view_model/auth_state.dart | 2 +- lib/view_model/auth_view_model.dart | 11 +- lib/view_model/backup_view_model.dart | 11 +- lib/view_model/buy/buy_amount_view_model.dart | 11 +- lib/view_model/buy/buy_item.dart | 4 +- lib/view_model/buy/buy_view_model.dart | 17 +- .../contact_list/contact_list_view_model.dart | 2 +- .../contact_list/contact_view_model.dart | 31 ++- .../dashboard/action_list_display_mode.dart | 2 +- .../dashboard/balance_view_model.dart | 75 ++++-- .../dashboard/dashboard_view_model.dart | 118 +++++---- lib/view_model/dashboard/filter_item.dart | 5 +- .../dashboard/formatted_item_list.dart | 2 +- lib/view_model/dashboard/order_list_item.dart | 4 +- lib/view_model/dashboard/trade_list_item.dart | 6 +- .../dashboard/transaction_list_item.dart | 14 +- lib/view_model/dashboard/wallet_balance.dart | 2 +- .../edit_backup_password_view_model.dart | 9 +- .../exchange/exchange_trade_view_model.dart | 32 ++- .../exchange/exchange_view_model.dart | 93 ++++--- .../ionia/ionia_account_view_model.dart | 7 +- .../ionia/ionia_auth_view_model.dart | 6 +- .../ionia/ionia_buy_card_view_model.dart | 7 +- .../ionia/ionia_custom_redeem_view_model.dart | 5 +- .../ionia/ionia_custom_tip_view_model.dart | 11 +- .../ionia_gift_card_details_view_model.dart | 11 +- .../ionia_gift_cards_list_view_model.dart | 19 +- .../ionia_payment_status_view_model.dart | 27 ++- .../ionia_purchase_merch_view_model.dart | 43 ++-- .../account_list_item.dart | 2 +- ...ero_account_edit_or_create_view_model.dart | 14 +- .../monero_account_list_view_model.dart | 14 +- .../node_create_or_edit_view_model.dart | 10 +- .../node_list/node_list_view_model.dart | 16 +- lib/view_model/order_details_view_model.dart | 24 +- lib/view_model/rescan_view_model.dart | 12 +- .../restore_from_backup_view_model.dart | 15 +- lib/view_model/send/output.dart | 34 +-- .../send/send_template_view_model.dart | 20 +- lib/view_model/send/send_view_model.dart | 77 ++++-- .../settings/choices_list_item.dart | 12 +- lib/view_model/settings/link_list_item.dart | 8 +- lib/view_model/settings/picker_list_item.dart | 24 +- .../settings/regular_list_item.dart | 4 +- .../settings/settings_view_model.dart | 34 ++- .../settings/switcher_list_item.dart | 6 +- .../settings/version_list_item.dart | 2 +- lib/view_model/support_view_model.dart | 34 +-- lib/view_model/trade_details_view_model.dart | 21 +- .../transaction_details_view_model.dart | 58 +++-- .../unspent_coins_details_view_model.dart | 16 +- .../unspent_coins/unspent_coins_item.dart | 12 +- .../unspent_coins_list_view_model.dart | 14 +- .../unspent_coins_switch_item.dart | 8 +- ...let_address_edit_or_create_view_model.dart | 24 +- .../wallet_address_list_item.dart | 12 +- .../wallet_address_list_view_model.dart | 63 +++-- lib/view_model/wallet_creation_vm.dart | 10 +- lib/view_model/wallet_keys_view_model.dart | 28 ++- .../wallet_list/wallet_list_item.dart | 6 +- .../wallet_list/wallet_list_view_model.dart | 10 +- lib/view_model/wallet_new_vm.dart | 12 +- .../wallet_restoration_from_keys_vm.dart | 15 +- .../wallet_restoration_from_seed_vm.dart | 11 +- lib/view_model/wallet_restore_view_model.dart | 25 +- pubspec_base.yaml | 107 +++++---- pubspec_description.yaml | 3 +- tool/configure.dart | 188 +++++++-------- tool/generate_localization.dart | 2 +- tool/localization/localization_constants.dart | 16 +- tool/utils/utils.dart | 2 +- 505 files changed, 6657 insertions(+), 5875 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 56738f471..524f70011 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,51 +1,73 @@ +include: package:lints/recommended.yaml + analyzer: - strong-mode: - implicit-casts: false - implicit-dynamic: false - exclude: [build/**, lib/generated/*.dart, lib/**.g.dart, cw_monero/ios/External/**, cw_shared_external/**, shared_external/**] + exclude: [ + build/**, + lib/**.g.dart, + cw_core/lib/**.g.dart, + cw_haven/lib/**.g.dart, + cw_monero/lib/**.g.dart, + lib/generated/*.dart, + cw_monero/ios/External/**, + cw_shared_external/**, + shared_external/**] + language: + strict-casts: true + strict-raw-types: true linter: rules: - - always_declare_return_types - - annotate_overrides - - avoid_empty_else - - avoid_init_to_null - - avoid_return_types_on_setters - - await_only_futures - - camel_case_types - cancel_subscriptions - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - invariant_booleans - - iterable_contains_unrelated_type - - library_names - - library_prefixes - - list_remove_unrelated_type - - literal_only_boolean_expressions - - non_constant_identifier_names - - one_member_abstracts - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_final_fields - - prefer_final_locals - - prefer_is_not_empty - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_init_formals - - unawaited_futures - - unnecessary_getters_setters - - unrelated_type_equality_checks - - valid_regexps \ No newline at end of file + + +# analyzer: +# strong-mode: +# implicit-casts: false +# implicit-dynamic: false +# exclude: [build/**, lib/generated/*.dart, lib/**.g.dart, cw_monero/ios/External/**, cw_shared_external/**, shared_external/**] + +# linter: +# rules: +# - always_declare_return_types +# - annotate_overrides +# - avoid_empty_else +# - avoid_init_to_null +# - avoid_return_types_on_setters +# - await_only_futures +# - camel_case_types +# - cancel_subscriptions +# - close_sinks +# - comment_references +# - constant_identifier_names +# - control_flow_in_finally +# - empty_catches +# - empty_constructor_bodies +# - empty_statements +# - hash_and_equals +# - invariant_booleans +# - iterable_contains_unrelated_type +# - library_names +# - library_prefixes +# - list_remove_unrelated_type +# - literal_only_boolean_expressions +# - non_constant_identifier_names +# - one_member_abstracts +# - only_throw_errors +# - overridden_fields +# - package_api_docs +# - package_names +# - package_prefixed_library_names +# - parameter_assignments +# - prefer_final_fields +# - prefer_final_locals +# - prefer_is_not_empty +# - slash_for_doc_comments +# - sort_constructors_first +# - sort_unnamed_constructors_first +# - test_types_in_equals +# - throw_in_finally +# - type_init_formals +# - unawaited_futures +# - unnecessary_getters_setters +# - unrelated_type_equality_checks +# - valid_regexps \ No newline at end of file diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index 6aa90e883..d06ffe402 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -8,7 +8,7 @@ String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { data: PaymentData(output: script), network: networkType) .data - .address; + .address!; } catch (_) {} try { @@ -16,8 +16,8 @@ String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { data: PaymentData(output: script), network: networkType) .data - .address; + .address!; } catch(_) {} - return null; + return ''; } \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 5210604ef..392771ab0 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -2,7 +2,7 @@ import 'dart:convert'; class BitcoinAddressRecord { BitcoinAddressRecord(this.address, - {this.index, this.isHidden = false, bool isUsed = false}) + {required this.index, this.isHidden = false, bool isUsed = false}) : _isUsed = isUsed; factory BitcoinAddressRecord.fromJSON(String jsonSource) { @@ -11,8 +11,8 @@ class BitcoinAddressRecord { return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool ?? false, - isUsed: decoded['isUsed'] as bool ?? false); + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false); } @override diff --git a/cw_bitcoin/lib/bitcoin_amount_format.dart b/cw_bitcoin/lib/bitcoin_amount_format.dart index 0c846596f..c72d21960 100644 --- a/cw_bitcoin/lib/bitcoin_amount_format.dart +++ b/cw_bitcoin/lib/bitcoin_amount_format.dart @@ -7,10 +7,10 @@ final bitcoinAmountFormat = NumberFormat() ..maximumFractionDigits = bitcoinAmountLength ..minimumFractionDigits = 1; -String bitcoinAmountToString({int amount}) => bitcoinAmountFormat.format( +String bitcoinAmountToString({required int amount}) => bitcoinAmountFormat.format( cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider)); -double bitcoinAmountToDouble({int amount}) => +double bitcoinAmountToDouble({required int amount}) => cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); int stringDoubleToBitcoinAmount(String amount) { diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 65f4ae0df..f4ebd7e5d 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -106,15 +106,18 @@ Future generateMnemonic( return result; } -Uint8List mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) { +Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { final pbkdf2 = cryptography.Pbkdf2( - macAlgorithm: cryptography.Hmac(cryptography.sha512), + macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); final text = normalizeText(mnemonic); - - return pbkdf2.deriveBitsSync(text.codeUnits, - nonce: cryptography.Nonce('electrum'.codeUnits)); + // pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) + final key = await pbkdf2.deriveKey( + secretKey: cryptography.SecretKey(text.codeUnits), + nonce: 'electrum'.codeUnits); + final bytes = await key.extractBytes(); + return Uint8List.fromList(bytes); } bool matchesAnyPrefix(String mnemonic) => diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index 7df93400a..bd8f1763c 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -2,9 +2,9 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.outputs, {this.priority, this.feeRate}); + BitcoinTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); final List outputs; - final BitcoinTransactionPriority priority; - final int feeRate; + final BitcoinTransactionPriority? priority; + final int? feeRate; } diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index f006e25e4..d82ea429e 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -2,7 +2,7 @@ import 'package:cw_core/transaction_priority.dart'; //import 'package:cake_wallet/generated/i18n.dart'; class BitcoinTransactionPriority extends TransactionPriority { - const BitcoinTransactionPriority({String title, int raw}) + const BitcoinTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); static const List all = [fast, medium, slow]; @@ -13,7 +13,7 @@ class BitcoinTransactionPriority extends TransactionPriority { static const BitcoinTransactionPriority fast = BitcoinTransactionPriority(title: 'Fast', raw: 2); - static BitcoinTransactionPriority deserialize({int raw}) { + static BitcoinTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return slow; @@ -22,7 +22,7 @@ class BitcoinTransactionPriority extends TransactionPriority { case 2: return fast; default: - return null; + throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); } } @@ -53,7 +53,7 @@ class BitcoinTransactionPriority extends TransactionPriority { } class LitecoinTransactionPriority extends BitcoinTransactionPriority { - const LitecoinTransactionPriority({String title, int raw}) + const LitecoinTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); static const List all = [fast, medium, slow]; @@ -64,7 +64,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { static const LitecoinTransactionPriority fast = LitecoinTransactionPriority(title: 'Fast', raw: 2); - static LitecoinTransactionPriority deserialize({int raw}) { + static LitecoinTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return slow; @@ -73,7 +73,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { case 2: return fast; default: - return null; + throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 243e342b7..c4675df1c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,4 +1,5 @@ import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -17,12 +18,13 @@ class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinWalletBase( - {@required String mnemonic, - @required String password, - @required WalletInfo walletInfo, - @required Box unspentCoinsInfo, - List initialAddresses, - ElectrumBalance initialBalance, + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + List? initialAddresses, + ElectrumBalance? initialBalance, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) : super( @@ -32,7 +34,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, networkType: bitcoin.bitcoin, initialAddresses: initialAddresses, - initialBalance: initialBalance) { + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.btc) { walletAddresses = BitcoinWalletAddresses( walletInfo, electrumClient: electrumClient, @@ -40,20 +44,40 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed( - mnemonicToSeedBytes(mnemonic), network: networkType) + sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType); } - static Future open({ - @required String name, - @required WalletInfo walletInfo, - @required Box unspentCoinsInfo, - @required String password, + static Future create({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + List? initialAddresses, + ElectrumBalance? initialBalance, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0 }) async { - final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); - await snp.load(); + return BitcoinWallet( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await mnemonicToSeedBytes(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex); + } + + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + }) async { + final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); return BitcoinWallet( mnemonic: snp.mnemonic, password: password, @@ -61,6 +85,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, + seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 33e79c102..de3fdfbca 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -16,13 +16,13 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, - {@required List initialAddresses, + {required bitcoin.HDWallet mainHd, + required bitcoin.HDWallet sideHd, + required bitcoin.NetworkType networkType, + required ElectrumClient electrumClient, + List? initialAddresses, int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0, - ElectrumClient electrumClient, - @required bitcoin.HDWallet mainHd, - @required bitcoin.HDWallet sideHd, - @required bitcoin.NetworkType networkType}) + int initialChangeAddressIndex = 0}) : super( walletInfo, initialAddresses: initialAddresses, @@ -34,6 +34,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses networkType: networkType); @override - String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + String getAddress({required int index, required bitcoin.HDWallet hd}) => generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); } \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index d3ade5c5e..82173b2d2 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -2,13 +2,13 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { - BitcoinNewWalletCredentials({String name, WalletInfo walletInfo}) + BitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo}) : super(name: name, walletInfo: walletInfo); } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { BitcoinRestoreWalletFromSeedCredentials( - {String name, String password, this.mnemonic, WalletInfo walletInfo}) + {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) : super(name: name, password: password, walletInfo: walletInfo); final String mnemonic; @@ -16,7 +16,7 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { BitcoinRestoreWalletFromWIFCredentials( - {String name, String password, this.wif, WalletInfo walletInfo}) + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) : super(name: name, password: password, walletInfo: walletInfo); final String wif; diff --git a/cw_bitcoin/lib/bitcoin_wallet_keys.dart b/cw_bitcoin/lib/bitcoin_wallet_keys.dart index 74212c74c..0a4afc10d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_keys.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_keys.dart @@ -1,7 +1,5 @@ -import 'package:flutter/foundation.dart'; - class BitcoinWalletKeys { - const BitcoinWalletKeys({@required this.wif, @required this.privateKey, @required this.publicKey}); + const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey}); final String wif; final String privateKey; diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 300f4daa9..398d68fc2 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -10,6 +10,7 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, @@ -25,10 +26,10 @@ class BitcoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials) async { - final wallet = BitcoinWallet( + final wallet = await BitcoinWalletBase.create( mnemonic: await generateMnemonic(), - password: credentials.password, - walletInfo: credentials.walletInfo, + password: credentials.password!, + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); @@ -41,9 +42,8 @@ class BitcoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType()), - orElse: () => null); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(name, getType()))!; final wallet = await BitcoinWalletBase.open( password: password, name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); @@ -68,10 +68,10 @@ class BitcoinWalletService extends WalletService< throw BitcoinMnemonicIsIncorrectException(); } - final wallet = BitcoinWallet( - password: credentials.password, + final wallet = await BitcoinWalletBase.create( + password: credentials.password!, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo, + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 8a8894fb8..678c7bc18 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -7,6 +7,7 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:collection/collection.dart'; String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); @@ -14,14 +15,20 @@ String jsonrpcparams(List params) { } String jsonrpc( - {String method, List params, int id, double version = 2.0}) => + {required String method, + required List params, + required int id, + double version = 2.0}) => '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { - SocketTask({this.completer, this.isSubscription, this.subject}); + SocketTask({ + required this.isSubscription, + this.completer, + this.subject}); - final Completer completer; - final BehaviorSubject subject; + final Completer? completer; + final BehaviorSubject? subject; final bool isSubscription; } @@ -36,18 +43,18 @@ class ElectrumClient { static const aliveTimerDuration = Duration(seconds: 2); bool get isConnected => _isConnected; - Socket socket; - void Function(bool) onConnectionStatusChange; + Socket? socket; + void Function(bool)? onConnectionStatusChange; int _id; final Map _tasks; bool _isConnected; - Timer _aliveTimer; + Timer? _aliveTimer; String unterminatedString; Future connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port); - Future connect({@required String host, @required int port}) async { + Future connect({required String host, required int port}) async { try { await socket?.close(); } catch (_) {} @@ -56,10 +63,10 @@ class ElectrumClient { timeout: connectionTimeout, onBadCertificate: (_) => true); _setIsConnected(true); - socket.listen((Uint8List event) { + socket!.listen((Uint8List event) { try { final response = - json.decode(utf8.decode(event.toList())) as Map; + json.decode(utf8.decode(event.toList())) as Map; _handleResponse(response); } on FormatException catch (e) { final msg = e.message.toLowerCase(); @@ -75,12 +82,12 @@ class ElectrumClient { if (isJSONStringCorrect(unterminatedString)) { final response = - json.decode(unterminatedString) as Map; + json.decode(unterminatedString) as Map; _handleResponse(response); unterminatedString = ''; } } on TypeError catch (e) { - if (!e.toString().contains('Map')) { + if (!e.toString().contains('Map') || !e.toString().contains('Map')) { return; } @@ -89,9 +96,10 @@ class ElectrumClient { if (isJSONStringCorrect(unterminatedString)) { final response = - json.decode(unterminatedString) as Map; + json.decode(unterminatedString) as Map; _handleResponse(response); - unterminatedString = null; + // unterminatedString = null; + unterminatedString = ''; } } catch (e) { print(e.toString()); @@ -207,7 +215,7 @@ class ElectrumClient { }); Future> getTransactionRaw( - {@required String hash}) async => + {required String hash}) async => call(method: 'blockchain.transaction.get', params: [hash, true]) .then((dynamic result) { if (result is Map) { @@ -218,7 +226,7 @@ class ElectrumClient { }); Future getTransactionHex( - {@required String hash}) async => + {required String hash}) async => call(method: 'blockchain.transaction.get', params: [hash, false]) .then((dynamic result) { if (result is String) { @@ -229,7 +237,7 @@ class ElectrumClient { }); Future broadcastTransaction( - {@required String transactionRaw}) async => + {required String transactionRaw}) async => call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) .then((dynamic result) { if (result is String) { @@ -240,16 +248,16 @@ class ElectrumClient { }); Future> getMerkle( - {@required String hash, @required int height}) async => + {required String hash, required int height}) async => await call( method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map; - Future> getHeader({@required int height}) async => + Future> getHeader({required int height}) async => await call(method: 'blockchain.block.get_header', params: [height]) as Map; - Future estimatefee({@required int p}) => + Future estimatefee({required int p}) => call(method: 'blockchain.estimatefee', params: [p]) .then((dynamic result) { if (result is double) { @@ -266,13 +274,26 @@ class ElectrumClient { Future>> feeHistogram() => call(method: 'mempool.get_fee_histogram').then((dynamic result) { if (result is List) { - return result.map((dynamic e) { - if (e is List) { - return e.map((dynamic ee) => ee is int ? ee : null).toList(); - } + // return result.map((dynamic e) { + // if (e is List) { + // return e.map((dynamic ee) => ee is int ? ee : null).toList(); + // } - return null; - }).toList(); + // return null; + // }).toList(); + final histogram = >[]; + for (final e in result) { + if (e is List) { + final eee = []; + for (final ee in e) { + if (ee is int) { + eee.add(ee); + } + } + histogram.add(eee); + } + } + return histogram; } return []; @@ -299,7 +320,7 @@ class ElectrumClient { } } - BehaviorSubject scripthashUpdate(String scripthash) { + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( id: 'blockchain.scripthash.subscribe:$scripthash', @@ -307,14 +328,14 @@ class ElectrumClient { params: [scripthash]); } - BehaviorSubject subscribe( - {@required String id, - @required String method, + BehaviorSubject? subscribe( + {required String id, + required String method, List params = const []}) { try { final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); - socket.write(jsonrpc(method: method, id: _id, params: params)); + socket!.write(jsonrpc(method: method, id: _id, params: params)); return subscription; } catch(e) { @@ -323,18 +344,18 @@ class ElectrumClient { } } - Future call({String method, List params = const []}) async { + Future call({required String method, List params = const []}) async { final completer = Completer(); _id += 1; final id = _id; _registryTask(id, completer); - socket.write(jsonrpc(method: method, id: id, params: params)); + socket!.write(jsonrpc(method: method, id: id, params: params)); return completer.future; } Future callWithTimeout( - {String method, + {required String method, List params = const [], int timeout = 2000}) async { try { @@ -342,7 +363,7 @@ class ElectrumClient { _id += 1; final id = _id; _registryTask(id, completer); - socket.write(jsonrpc(method: method, id: id, params: params)); + socket!.write(jsonrpc(method: method, id: id, params: params)); Timer(Duration(milliseconds: timeout), () { if (!completer.isCompleted) { completer.completeError(RequestFailedTimeoutException(method, id)); @@ -356,35 +377,35 @@ class ElectrumClient { } Future close() async { - _aliveTimer.cancel(); - await socket.close(); + _aliveTimer?.cancel(); + await socket?.close(); onConnectionStatusChange = null; } - void _registryTask(int id, Completer completer) => _tasks[id.toString()] = + void _registryTask(int id, Completer completer) => _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false); - void _regisrySubscription(String id, BehaviorSubject subject) => + void _regisrySubscription(String id, BehaviorSubject subject) => _tasks[id] = SocketTask(subject: subject, isSubscription: true); - void _finish(String id, Object data) { + void _finish(String id, Object? data) { if (_tasks[id] == null) { return; } if (!(_tasks[id]?.completer?.isCompleted ?? false)) { - _tasks[id]?.completer?.complete(data); + _tasks[id]?.completer!.complete(data); } if (!(_tasks[id]?.isSubscription ?? false)) { - _tasks[id] = null; + _tasks.remove(id); } else { - _tasks[id].subject.add(data); + _tasks[id]?.subject?.add(data); } } void _methodHandler( - {@required String method, @required Map request}) { + {required String method, required Map request}) { switch (method) { case 'blockchain.scripthash.subscribe': final params = request['params'] as List; @@ -406,7 +427,7 @@ class ElectrumClient { _isConnected = isConnected; } - void _handleResponse(Map response) { + void _handleResponse(Map response) { final method = response['method']; final id = response['id'] as String; final result = response['result']; diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index ec71977ad..a26b79ddb 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -4,10 +4,10 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { - const ElectrumBalance({@required this.confirmed, @required this.unconfirmed}) + const ElectrumBalance({required this.confirmed, required this.unconfirmed}) : super(confirmed, unconfirmed); - factory ElectrumBalance.fromJSON(String jsonSource) { + static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { return null; } @@ -15,8 +15,8 @@ class ElectrumBalance extends Balance { final decoded = json.decode(jsonSource) as Map; return ElectrumBalance( - confirmed: decoded['confirmed'] as int ?? 0, - unconfirmed: decoded['unconfirmed'] as int ?? 0); + confirmed: decoded['confirmed'] as int? ?? 0, + unconfirmed: decoded['unconfirmed'] as int? ?? 0); } final int confirmed; diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 94f328900..f8662eb95 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -17,7 +17,7 @@ class ElectrumTransactionHistory = ElectrumTransactionHistoryBase abstract class ElectrumTransactionHistoryBase extends TransactionHistoryBase with Store { ElectrumTransactionHistoryBase( - {@required this.walletInfo, @required String password}) + {required this.walletInfo, required String password}) : _password = password, _height = 0 { transactions = ObservableMap(); @@ -56,18 +56,18 @@ abstract class ElectrumTransactionHistoryBase await save(); } - Future> _read() async { + Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$_transactionsHistoryFileName'; final content = await read(path: path, password: _password); - return json.decode(content) as Map; + return json.decode(content) as Map; } Future _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map ?? {}; + final txs = content['transactions'] as Map ?? {}; txs.entries.forEach((entry) { final val = entry.value; @@ -93,11 +93,11 @@ abstract class ElectrumTransactionHistoryBase transactions[transaction.id] = transaction; } else { final originalTx = transactions[transaction.id]; - originalTx.confirmations = transaction.confirmations; - originalTx.amount = transaction.amount; - originalTx.height = transaction.height; - originalTx.date ??= transaction.date; - originalTx.isPending = transaction.isPending; + originalTx?.confirmations = transaction.confirmations; + originalTx?.amount = transaction.amount; + originalTx?.height = transaction.height; + originalTx?.date ??= transaction.date; + originalTx?.isPending = transaction.isPending; } } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 491ebddfb..6e85a2f88 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -10,23 +10,26 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_core/wallet_type.dart'; class ElectrumTransactionBundle { - ElectrumTransactionBundle(this.originalTransaction, {this.ins, this.time, this.confirmations}); + ElectrumTransactionBundle(this.originalTransaction, + {required this.ins, + required this.confirmations, + this.time}); final bitcoin.Transaction originalTransaction; final List ins; - final int time; + final int? time; final int confirmations; } class ElectrumTransactionInfo extends TransactionInfo { ElectrumTransactionInfo(this.type, - {@required String id, - @required int height, - @required int amount, - @required int fee, - @required TransactionDirection direction, - @required bool isPending, - @required DateTime date, - @required int confirmations}) { + {required String id, + required int height, + required int amount, + int? fee, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int confirmations}) { this.id = id; this.height = height; this.amount = amount; @@ -39,15 +42,15 @@ class ElectrumTransactionInfo extends TransactionInfo { factory ElectrumTransactionInfo.fromElectrumVerbose( Map obj, WalletType type, - {@required List addresses, @required int height}) { + {required List addresses, required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; - final vins = obj['vin'] as List ?? []; - final vout = (obj['vout'] as List ?? []); + final vins = obj['vin'] as List? ?? []; + final vout = (obj['vout'] as List? ?? []); final date = obj['time'] is int ? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000) : DateTime.now(); - final confirmations = obj['confirmations'] as int ?? 0; + final confirmations = obj['confirmations'] as int? ?? 0; var direction = TransactionDirection.incoming; var inputsAmount = 0; var amount = 0; @@ -57,21 +60,21 @@ class ElectrumTransactionInfo extends TransactionInfo { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; final outAddresses = - (out['scriptPubKey']['addresses'] as List)?.toSet(); + (out['scriptPubKey']['addresses'] as List?)?.toSet(); inputsAmount += - stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString()); + stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); - if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) { + if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; } } for (dynamic out in vout) { final outAddresses = - out['scriptPubKey']['addresses'] as List ?? []; + out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); final value = stringDoubleToBitcoinAmount( - (out['value'] as double ?? 0.0).toString()); + (out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -97,10 +100,10 @@ class ElectrumTransactionInfo extends TransactionInfo { ElectrumTransactionBundle bundle, WalletType type, bitcoin.NetworkType networkType, - {@required Set addresses, - int height}) { + {required Set addresses, + required int height}) { final date = bundle.time != null - ? DateTime.fromMillisecondsSinceEpoch(bundle.time * 1000) + ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) : DateTime.now(); var direction = TransactionDirection.incoming; var amount = 0; @@ -111,21 +114,21 @@ class ElectrumTransactionInfo extends TransactionInfo { final input = bundle.originalTransaction.ins[i]; final inputTransaction = bundle.ins[i]; final vout = input.index; - final outTransaction = inputTransaction.outs[vout]; - final address = addressFromOutput(outTransaction.script, networkType); - inputAmount += outTransaction.value; + final outTransaction = inputTransaction.outs[vout!]; + final address = addressFromOutput(outTransaction.script!, networkType); + inputAmount += outTransaction.value!; if (addresses.contains(address)) { direction = TransactionDirection.outgoing; } } for (final out in bundle.originalTransaction.outs) { - totalOutAmount += out.value; - final address = addressFromOutput(out.script, networkType); + totalOutAmount += out.value!; + final address = addressFromOutput(out.script!, networkType); final addressExists = addresses.contains(address); if ((direction == TransactionDirection.incoming && addressExists) || (direction == TransactionDirection.outgoing && !addressExists)) { - amount += out.value; + amount += out.value!; } } @@ -142,7 +145,7 @@ class ElectrumTransactionInfo extends TransactionInfo { } factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, - {List addresses, int height, int timestamp, int confirmations}) { + {List? addresses, required int height, int? timestamp, required int confirmations}) { final tx = bitcoin.Transaction.fromHex(hex); var exist = false; var amount = 0; @@ -155,7 +158,7 @@ class ElectrumTransactionInfo extends TransactionInfo { exist = addresses.contains(p2pkh.data.address); if (exist) { - amount += out.value; + amount += out.value!; } } catch (_) {} }); @@ -191,15 +194,15 @@ class ElectrumTransactionInfo extends TransactionInfo { final WalletType type; - String _fiatAmount; + String? _fiatAmount; @override String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; @override - String feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee))} ${walletTypeToCryptoCurrency(type).title}' + String? feeFormatted() => fee != null + ? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override @@ -225,7 +228,9 @@ class ElectrumTransactionInfo extends TransactionInfo { m['id'] = id; m['height'] = height; m['amount'] = amount; - m['direction'] = direction.index; + // FIX-ME: Hardcoded value + // m['direction'] = direction.index; + m['direction'] = 0; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; m['confirmations'] = confirmations; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a4db681f3..516b44f56 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -34,6 +34,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:hex/hex.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:collection/collection.dart'; part 'electrum_wallet.g.dart'; @@ -42,31 +43,34 @@ class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; abstract class ElectrumWalletBase extends WalletBase with Store { ElectrumWalletBase( - {@required String password, - @required WalletInfo walletInfo, - @required Box unspentCoinsInfo, - @required List initialAddresses, - @required this.networkType, - @required this.mnemonic, - ElectrumClient electrumClient, - ElectrumBalance initialBalance}) - : hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), - network: networkType) + {required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required this.networkType, + required this.mnemonic, + required Uint8List seedBytes, + List? initialAddresses, + ElectrumClient? electrumClient, + ElectrumBalance? initialBalance, + CryptoCurrency? currency}) + : hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) .derivePath("m/0'/0"), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], _isTransactionUpdating = false, + unspentCoins = [], + _scripthashesUpdateSubject = {}, + balance = ObservableMap.of( + currency != null + ? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0)} + : {}), + this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { - balance = ObservableMap.of({ - currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0)}); this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; - this.unspentCoinsInfo = unspentCoinsInfo; transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); - unspentCoins = []; - _scripthashesUpdateSubject = {}; } static int estimatedTransactionSize(int inputsCount, int outputsCounts) => @@ -75,15 +79,15 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo; @override - ElectrumWalletAddresses walletAddresses; + late ElectrumWalletAddresses walletAddresses; @override @observable - ObservableMap balance; + late ObservableMap balance; @override @observable @@ -98,7 +102,7 @@ abstract class ElectrumWalletBase extends WalletBase scriptHash(addr.address, networkType: networkType)) .toList(); - String get xpub => hd.base58; + String get xpub => hd.base58!; @override String get seed => mnemonic; @@ -107,12 +111,12 @@ abstract class ElectrumWalletBase extends WalletBase BitcoinWalletKeys( - wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); + wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); String _password; List unspentCoins; List _feeRates; - Map> _scripthashesUpdateSubject; + Map?> _scripthashesUpdateSubject; bool _isTransactionUpdating; Future init() async { @@ -137,7 +141,8 @@ abstract class ElectrumWalletBase extends WalletBase _feeRates = await electrumClient.feeRates()); syncStatus = SyncedSyncStatus(); - } catch (e) { + } catch (e, stacktrace) { + print(stacktrace); print(e.toString()); syncStatus = FailedSyncStatus(); } @@ -145,7 +150,7 @@ abstract class ElectrumWalletBase extends WalletBase connectToNode({@required Node node}) async { + Future connectToNode({required Node node}) async { try { syncStatus = ConnectingSyncStatus(); await electrumClient.connectToUri(node.uri); @@ -187,7 +192,7 @@ abstract class ElectrumWalletBase extends WalletBase item.sendAll - || item.formattedCryptoAmount <= 0)) { + || item.formattedCryptoAmount! <= 0)) { throw BitcoinTransactionWrongBalanceException(currency); } credentialsAmount = outputs.fold(0, (acc, value) => - acc + value.formattedCryptoAmount); + acc + value.formattedCryptoAmount!); if (allAmount - credentialsAmount < minAmount) { throw BitcoinTransactionWrongBalanceException(currency); @@ -210,7 +215,7 @@ abstract class ElectrumWalletBase extends WalletBase allAmount) { @@ -233,7 +238,7 @@ abstract class ElectrumWalletBase extends WalletBase balance[currency].confirmed || totalAmount > allInputsAmount) { + if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { throw BitcoinTransactionWrongBalanceException(currency); } @@ -298,11 +303,11 @@ abstract class ElectrumWalletBase extends WalletBase + bitcoin.ECPair keyPairFor({required int index}) => generateKeyPair(hd: hd, index: index, network: networkType); @override - Future rescan({int height}) async => throw UnimplementedError(); + Future rescan({required int height}) async => throw UnimplementedError(); @override Future close() async { try { - await electrumClient?.close(); + await electrumClient.close(); } catch (_) {} } @@ -498,10 +503,9 @@ abstract class ElectrumWalletBase extends WalletBase element.hash.contains(coin?.hash)); + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); - if (existUnspentCoins?.isEmpty ?? true) { + if (existUnspentCoins.isEmpty) { keys.add(element.key); } }); @@ -516,7 +520,7 @@ abstract class ElectrumWalletBase extends WalletBase getTransactionExpanded( - {@required String hash, @required int height}) async { + {required String hash, required int height}) async { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); final transactionHex = verboseTransaction['hex'] as String; final original = bitcoin.Transaction.fromHex(transactionHex); @@ -525,7 +529,7 @@ abstract class ElectrumWalletBase extends WalletBase fetchTransactionInfo( - {@required String hash, @required int height}) async { + {required String hash, required int height}) async { final tx = await getTransactionExpanded(hash: hash, height: height); final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); return ElectrumTransactionInfo.fromElectrumBundle( @@ -567,7 +571,7 @@ abstract class ElectrumWalletBase extends WalletBase initialAddresses, + {required this.mainHd, + required this.sideHd, + required this.electrumClient, + required this.networkType, + List? initialAddresses, int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0, - this.mainHd, - this.sideHd, - this.electrumClient, - this.networkType}) + int initialChangeAddressIndex = 0}) : addresses = ObservableList.of( (initialAddresses ?? []).toSet()), receiveAddresses = ObservableList.of( @@ -31,10 +30,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { (initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .toSet()), - super(walletInfo) { - currentReceiveAddressIndex = initialRegularAddressIndex; - currentChangeAddressIndex = initialChangeAddressIndex; - } + currentReceiveAddressIndex = initialRegularAddressIndex, + currentChangeAddressIndex = initialChangeAddressIndex, + super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -124,17 +122,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Future generateNewAddress( - {bool isHidden = false, bitcoin.HDWallet hd}) async { + {bitcoin.HDWallet? hd, bool isHidden = false}) async { currentReceiveAddressIndex += 1; + // FIX-ME: Check logic for whichi HD should be used here ??? final address = BitcoinAddressRecord( - getAddress(index: currentReceiveAddressIndex, hd: hd), + getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd), index: currentReceiveAddressIndex, isHidden: isHidden); addresses.add(address); return address; } - String getAddress({@required int index, @required bitcoin.HDWallet hd}) => ''; + String getAddress({required int index, required bitcoin.HDWallet hd}) => ''; @override Future updateAddressesInBox() async { @@ -239,7 +238,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Future> _createNewAddresses(int count, - {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { + {required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false}) async { final list = []; for (var i = startIndex; i < count + startIndex; i++) { diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 3bc1f0607..3755e7d18 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -6,7 +6,15 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; class ElectrumWallletSnapshot { - ElectrumWallletSnapshot(this.name, this.type, this.password); + ElectrumWallletSnapshot({ + required this.name, + required this.type, + required this.password, + required this.mnemonic, + required this.addresses, + required this.balance, + required this.regularAddressIndex, + required this.changeAddressIndex}); final String name; final String password; @@ -18,28 +26,34 @@ class ElectrumWallletSnapshot { int regularAddressIndex; int changeAddressIndex; - Future load() async { - try { - final path = await pathForWallet(name: name, type: type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List ?? []; - mnemonic = data['mnemonic'] as String; - addresses = addressesTmp - .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr)) - .toList(); - balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? - ElectrumBalance(confirmed: 0, unconfirmed: 0); - regularAddressIndex = 0; - changeAddressIndex = 0; + static Future load(String name, WalletType type, String password) async { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final addressesTmp = data['addresses'] as List? ?? []; + final mnemonic = data['mnemonic'] as String; + final addresses = addressesTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .toList(); + final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? + ElectrumBalance(confirmed: 0, unconfirmed: 0); + var regularAddressIndex = 0; + var changeAddressIndex = 0; - try { - regularAddressIndex = int.parse(data['account_index'] as String); - changeAddressIndex = int.parse(data['change_address_index'] as String); - } catch (_) {} - } catch (e) { - print(e); - } + try { + regularAddressIndex = int.parse(data['account_index'] as String? ?? '0'); + changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0'); + } catch (_) {} + + return ElectrumWallletSnapshot( + name: name, + type: type, + password: password, + mnemonic: mnemonic, + addresses: addresses, + balance: balance, + regularAddressIndex: regularAddressIndex, + changeAddressIndex: changeAddressIndex); } } diff --git a/cw_bitcoin/lib/file.dart b/cw_bitcoin/lib/file.dart index e046e74fe..8fd236ec3 100644 --- a/cw_bitcoin/lib/file.dart +++ b/cw_bitcoin/lib/file.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:cw_core/key.dart'; import 'package:encrypt/encrypt.dart' as encrypt; -import 'package:flutter/foundation.dart'; Future write( - {@required String path, - @required String password, - @required String data}) async { + {required String path, + required String password, + required String data}) async { final keys = extractKeys(password); final key = encrypt.Key.fromBase64(keys.first); final iv = encrypt.IV.fromBase64(keys.last); @@ -16,9 +15,9 @@ Future write( } Future writeData( - {@required String path, - @required String password, - @required String data}) async { + {required String path, + required String password, + required String data}) async { final keys = extractKeys(password); final key = encrypt.Key.fromBase64(keys.first); final iv = encrypt.IV.fromBase64(keys.last); @@ -27,7 +26,7 @@ Future writeData( f.writeAsStringSync(encrypted); } -Future read({@required String path, @required String password}) async { +Future read({required String path, required String password}) async { final file = File(path); if (!file.existsSync()) { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6f6ba3880..6bf1c5735 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,5 +1,6 @@ import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -20,12 +21,13 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase( - {@required String mnemonic, - @required String password, - @required WalletInfo walletInfo, - @required Box unspentCoinsInfo, - List initialAddresses, - ElectrumBalance initialBalance, + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + List? initialAddresses, + ElectrumBalance? initialBalance, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) : super( @@ -35,7 +37,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, networkType: litecoinNetwork, initialAddresses: initialAddresses, - initialBalance: initialBalance) { + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( walletInfo, electrumClient: electrumClient, @@ -44,19 +48,40 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet - .fromSeed(mnemonicToSeedBytes(mnemonic), network: networkType) + .fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType,); } - static Future open({ - @required String name, - @required WalletInfo walletInfo, - @required Box unspentCoinsInfo, - @required String password, + static Future create({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + List? initialAddresses, + ElectrumBalance? initialBalance, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0 }) async { - final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); - await snp.load(); + return LitecoinWallet( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await mnemonicToSeedBytes(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex); + } + + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + }) async { + final snp = await ElectrumWallletSnapshot.load (name, walletInfo.type, password); return LitecoinWallet( mnemonic: snp.mnemonic, password: password, @@ -64,6 +89,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, + seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 1a39b8452..a317fa9f2 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -16,13 +16,13 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, - {@required List initialAddresses, + {required bitcoin.HDWallet mainHd, + required bitcoin.HDWallet sideHd, + required bitcoin.NetworkType networkType, + required ElectrumClient electrumClient, + List? initialAddresses, int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0, - ElectrumClient electrumClient, - @required bitcoin.HDWallet mainHd, - @required bitcoin.HDWallet sideHd, - @required bitcoin.NetworkType networkType}) + int initialChangeAddressIndex = 0}) : super( walletInfo, initialAddresses: initialAddresses, @@ -34,6 +34,6 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses networkType: networkType); @override - String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + String getAddress({required int index, required bitcoin.HDWallet hd}) => generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); } \ No newline at end of file diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index f0b3f1693..2093647fd 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -10,6 +10,7 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:collection/collection.dart'; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, @@ -25,10 +26,10 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials) async { - final wallet = LitecoinWallet( + final wallet = await LitecoinWalletBase.create( mnemonic: await generateMnemonic(), - password: credentials.password, - walletInfo: credentials.walletInfo, + password: credentials.password!, + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); @@ -42,9 +43,8 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType()), - orElse: () => null); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(name, getType()))!; final wallet = await LitecoinWalletBase.open( password: password, name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); @@ -69,10 +69,10 @@ class LitecoinWalletService extends WalletService< throw BitcoinMnemonicIsIncorrectException(); } - final wallet = LitecoinWallet( - password: credentials.password, + final wallet = await LitecoinWalletBase.create( + password: credentials.password!, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo, + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index b9f754c72..e2dc10bfb 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,5 +1,4 @@ import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; -import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; @@ -10,9 +9,9 @@ import 'package:cw_core/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction(this._tx, this.type, - {@required this.electrumClient, - @required this.amount, - @required this.fee}) + {required this.electrumClient, + required this.amount, + required this.fee}) : _listeners = []; final WalletType type; diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart index b1025f66b..76a1bfcf0 100644 --- a/cw_bitcoin/lib/script_hash.dart +++ b/cw_bitcoin/lib/script_hash.dart @@ -1,8 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:crypto/crypto.dart'; -String scriptHash(String address, {@required bitcoin.NetworkType networkType}) { +String scriptHash(String address, {required bitcoin.NetworkType networkType}) { final outputScript = bitcoin.Address.addressToOutputScript(address, networkType); final parts = sha256.convert(outputScript).toString().split(''); diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 3a638555a..0d5a413b3 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -5,51 +5,51 @@ import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; bitcoin.PaymentData generatePaymentData( - {@required bitcoin.HDWallet hd, @required int index}) => + {required bitcoin.HDWallet hd, required int index}) => PaymentData( - pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))); + pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); bitcoin.ECPair generateKeyPair( - {@required bitcoin.HDWallet hd, - @required int index, - bitcoin.NetworkType network}) => - bitcoin.ECPair.fromWIF(hd.derive(index).wif, network: network); + {required bitcoin.HDWallet hd, + required int index, + required bitcoin.NetworkType network}) => + bitcoin.ECPair.fromWIF(hd.derive(index).wif!, network: network); String generateP2WPKHAddress( - {@required bitcoin.HDWallet hd, - @required int index, - bitcoin.NetworkType networkType}) => + {required bitcoin.HDWallet hd, + required int index, + required bitcoin.NetworkType networkType}) => bitcoin .P2WPKH( data: PaymentData( pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), network: networkType) .data - .address; + .address!; String generateP2WPKHAddressByPath( - {@required bitcoin.HDWallet hd, - @required String path, - bitcoin.NetworkType networkType}) => + {required bitcoin.HDWallet hd, + required String path, + required bitcoin.NetworkType networkType}) => bitcoin .P2WPKH( data: PaymentData( pubkey: - Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey))), + Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey!))), network: networkType) .data - .address; + .address!; String generateP2PKHAddress( - {@required bitcoin.HDWallet hd, - @required int index, - bitcoin.NetworkType networkType}) => + {required bitcoin.HDWallet hd, + required int index, + required bitcoin.NetworkType networkType}) => bitcoin .P2PKH( data: PaymentData( pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), network: networkType) .data - .address; + .address!; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 504c92fab..9207fc209 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -7,64 +7,64 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "4.7.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.3.1" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.6.5" + version: "1.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.9.0" bech32: dependency: transitive description: path: "." - ref: cake - resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2" + ref: "cake-0.2.1" + resolved-ref: cafd1c270641e95017d57d69f55cca9831d4db56 url: "https://github.com/cake-tech/bech32.git" source: git - version: "0.2.0" + version: "0.2.1" bip32: dependency: transitive description: name: bip32 url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "2.0.0" bip39: dependency: transitive description: name: bip39 url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.6" bitcoin_flutter: dependency: "direct main" description: path: "." - ref: cake - resolved-ref: cbabfd87b6ce3cae6051a3e86ddb56e7a934e188 + ref: cake-update-v2 + resolved-ref: "8f86453761c0c26e368392d0ff2c6f12f3b7397b" url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.0.2" @@ -81,133 +81,119 @@ packages: name: bs58check url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" build: dependency: transitive description: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.10" + version: "3.1.0" build_resolvers: dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "7.2.4" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "5.1.1" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "8.4.1" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.3.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.2" cryptography: dependency: "direct main" description: name: cryptography url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "2.0.5" cw_core: dependency: "direct main" description: @@ -221,35 +207,28 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.12" - dartx: - dependency: transitive - description: - name: dartx - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" + version: "2.2.4" encrypt: dependency: "direct main" description: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.0.3" + version: "5.0.1" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" file: dependency: transitive description: @@ -263,7 +242,7 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.11" + version: "1.0.1" flutter: dependency: "direct main" description: flutter @@ -275,12 +254,19 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "1.1.0+2" + version: "2.0.6+4" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" glob: dependency: transitive description: @@ -294,49 +280,49 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.1.0" hex: dependency: transitive description: name: hex url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0" hive: dependency: transitive description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.4+1" + version: "2.2.3" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "1.1.3" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.1" intl: dependency: "direct main" description: @@ -350,7 +336,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.3" js: dependency: transitive description: @@ -364,7 +350,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.7.0" logging: dependency: transitive description: @@ -378,14 +364,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.8.0" mime: dependency: transitive description: @@ -399,63 +392,77 @@ packages: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "1.2.1+4" + version: "2.1.0" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.7+3" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.28" + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+8" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.5" + version: "2.1.3" pedantic: dependency: transitive description: @@ -476,14 +483,14 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "3.6.2" pool: dependency: transitive description: @@ -511,35 +518,28 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.5" + version: "1.2.1" rxdart: dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.26.0" + version: "0.27.5" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -551,14 +551,21 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.2.5" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -586,35 +593,28 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - time: - dependency: transitive - description: - name: time - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.1" + version: "0.4.12" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" typed_data: dependency: transitive description: @@ -635,7 +635,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" watcher: dependency: transitive description: @@ -649,21 +649,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.2.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "3.0.0" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0+2" yaml: dependency: transitive description: @@ -672,5 +672,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=3.0.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index d7c324775..455ceb4a7 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -6,35 +6,35 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.17.5 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - path_provider: ^1.4.0 - http: ^0.12.0+2 - mobx: ^1.2.1+2 - flutter_mobx: ^1.1.0+2 + path_provider: ^2.0.11 + http: ^0.13.4 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 intl: ^0.17.0 cw_core: path: ../cw_core bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake - rxdart: ^0.26.0 + ref: cake-update-v2 + rxdart: ^0.27.5 unorm_dart: ^0.2.0 - cryptography: ^1.4.0 - encrypt: ^4.0.0 + cryptography: ^2.0.5 + encrypt: ^5.0.1 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.10.3 - build_resolvers: ^1.3.10 - mobx_codegen: ^1.1.0+1 - hive_generator: ^0.8.1 + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/account.dart b/cw_core/lib/account.dart index 5bb002945..1633ee189 100644 --- a/cw_core/lib/account.dart +++ b/cw_core/lib/account.dart @@ -1,7 +1,7 @@ class Account { - Account({this.id, this.label}); + Account({required this.id, required this.label}); - Account.fromMap(Map map) + Account.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.label = (map['label'] ?? '') as String; diff --git a/cw_core/lib/account_list.dart b/cw_core/lib/account_list.dart index 4ac5faa3e..f49f69aa9 100644 --- a/cw_core/lib/account_list.dart +++ b/cw_core/lib/account_list.dart @@ -8,9 +8,9 @@ abstract class AccountList { List getAll(); - Future addAccount({String label}); + Future addAccount({required String label}); - Future setLabelAccount({int accountIndex, String label}); + Future setLabelAccount({required int accountIndex, required String label}); void refresh(); } diff --git a/cw_core/lib/crypto_amount_format.dart b/cw_core/lib/crypto_amount_format.dart index 649ac45f5..a5c06e485 100644 --- a/cw_core/lib/crypto_amount_format.dart +++ b/cw_core/lib/crypto_amount_format.dart @@ -1 +1 @@ -double cryptoAmountToDouble({num amount, num divider}) => amount / divider; \ No newline at end of file +double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider; \ No newline at end of file diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 271066f58..1da753667 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -5,12 +5,17 @@ part 'crypto_currency.g.dart'; @HiveType(typeId: 0) class CryptoCurrency extends EnumerableItem with Serializable { - const CryptoCurrency({final String title, this.tag, this.name, this.iconPath, final int raw}) + const CryptoCurrency({ + String title = '', + int raw = -1, + this.name, + this.iconPath, + this.tag,}) : super(title: title, raw: raw); - final String tag; - final String name; - final String iconPath; + final String? tag; + final String? name; + final String? iconPath; static const all = [ CryptoCurrency.xmr, @@ -97,10 +102,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const zen = CryptoCurrency(title: 'ZEN', iconPath: 'assets/images/zen_icon.png', raw: 44); static const xvg = CryptoCurrency(title: 'XVG', name: 'Verge', iconPath: 'assets/images/xvg_icon.png', raw: 45); - - - - static CryptoCurrency deserialize({int raw}) { + static CryptoCurrency deserialize({required int raw}) { switch (raw) { case 0: return CryptoCurrency.xmr; @@ -195,7 +197,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { case 45: return CryptoCurrency.xvg; default: - return null; + throw Exception('Unexpected token: $raw for CryptoCurrency deserialize'); } } @@ -294,7 +296,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { case 'xvg': return CryptoCurrency.xvg; default: - return null; + throw Exception('Unexpected token: $raw for CryptoCurrency fromString'); } } diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index b6d1f18c0..3904fc049 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -12,6 +12,6 @@ CryptoCurrency currencyForWalletType(WalletType type) { case WalletType.haven: return CryptoCurrency.xhv; default: - return null; + throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } diff --git a/cw_core/lib/enumerable_item.dart b/cw_core/lib/enumerable_item.dart index e9deb3056..e7a863909 100644 --- a/cw_core/lib/enumerable_item.dart +++ b/cw_core/lib/enumerable_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; abstract class EnumerableItem { - const EnumerableItem({@required this.title, @required this.raw}); + const EnumerableItem({required this.title, required this.raw}); final T raw; final String title; @@ -11,6 +11,6 @@ abstract class EnumerableItem { } mixin Serializable on EnumerableItem { - static Serializable deserialize({T raw}) => null; + static Serializable deserialize({required T raw}) => throw Exception('Unimplemented'); T serialize() => raw; } diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 99e834c58..e350a19cb 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -85,7 +85,7 @@ final dates = { "2020-11": 2220000 }; -int getMoneroHeigthByDate({DateTime date}) { +int getMoneroHeigthByDate({required DateTime date}) { final raw = '${date.year}' + '-' + '${date.month}'; final lastHeight = dates.values.last; int startHeight; @@ -105,7 +105,7 @@ int getMoneroHeigthByDate({DateTime date}) { final daysHeight = (differenceInDays * heightPerDay).round(); height = endHeight + daysHeight; } else { - startHeight = dates[raw]; + startHeight = dates[raw]!; final index = dates.values.toList().indexOf(startHeight); endHeight = dates.values.toList()[index + 1]; final heightPerDay = ((endHeight - startHeight) / 31).round(); diff --git a/cw_core/lib/key.dart b/cw_core/lib/key.dart index bdabe7852..6973b7a68 100644 --- a/cw_core/lib/key.dart +++ b/cw_core/lib/key.dart @@ -16,14 +16,14 @@ List extractKeys(String key) { return [_key, iv]; } -Future encode({encrypt.Key key, encrypt.IV iv, String data}) async { +Future encode({required encrypt.Key key, required encrypt.IV iv, required String data}) async { final encrypter = encrypt.Encrypter(encrypt.Salsa20(key)); final encrypted = encrypter.encrypt(data, iv: iv); return encrypted.base64; } -Future decode({String password, String data}) async { +Future decode({required String password, required String data}) async { final keys = extractKeys(password); final key = encrypt.Key.fromBase64(keys.first); final iv = encrypt.IV.fromBase64(keys.last); diff --git a/cw_core/lib/monero_amount_format.dart b/cw_core/lib/monero_amount_format.dart index 92bf0da25..912527b4e 100644 --- a/cw_core/lib/monero_amount_format.dart +++ b/cw_core/lib/monero_amount_format.dart @@ -7,12 +7,12 @@ final moneroAmountFormat = NumberFormat() ..maximumFractionDigits = moneroAmountLength ..minimumFractionDigits = 1; -String moneroAmountToString({int amount}) => moneroAmountFormat +String moneroAmountToString({required int amount}) => moneroAmountFormat .format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)) .replaceAll(',', ''); -double moneroAmountToDouble({int amount}) => +double moneroAmountToDouble({required int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); -int moneroParseAmount({String amount}) => +int moneroParseAmount({required String amount}) => (double.parse(amount) * moneroAmountDivider).toInt(); diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 8f0ae610e..7d569ef2f 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -1,17 +1,16 @@ import 'package:cw_core/balance.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { - MoneroBalance({@required this.fullBalance, @required this.unlockedBalance}) + MoneroBalance({required this.fullBalance, required this.unlockedBalance}) : formattedFullBalance = moneroAmountToString(amount: fullBalance), formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), super(unlockedBalance, fullBalance); MoneroBalance.fromString( - {@required this.formattedFullBalance, - @required this.formattedUnlockedBalance}) + {required this.formattedFullBalance, + required this.formattedUnlockedBalance}) : fullBalance = moneroParseAmount(amount: formattedFullBalance), unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), super(moneroParseAmount(amount: formattedUnlockedBalance), diff --git a/cw_core/lib/monero_transaction_priority.dart b/cw_core/lib/monero_transaction_priority.dart index fe937cb1f..1aca5dd8c 100644 --- a/cw_core/lib/monero_transaction_priority.dart +++ b/cw_core/lib/monero_transaction_priority.dart @@ -4,7 +4,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/enumerable_item.dart'; class MoneroTransactionPriority extends TransactionPriority { - const MoneroTransactionPriority({String title, int raw}) + const MoneroTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); static const all = [ @@ -37,7 +37,7 @@ class MoneroTransactionPriority extends TransactionPriority { } } - static MoneroTransactionPriority deserialize({int raw}) { + static MoneroTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return slow; @@ -50,7 +50,7 @@ class MoneroTransactionPriority extends TransactionPriority { case 4: return fastest; default: - return null; + throw Exception('Unexpected token: $raw for MoneroTransactionPriority deserialize'); } } diff --git a/cw_core/lib/monero_wallet_keys.dart b/cw_core/lib/monero_wallet_keys.dart index f0a96bfd3..1435002a8 100644 --- a/cw_core/lib/monero_wallet_keys.dart +++ b/cw_core/lib/monero_wallet_keys.dart @@ -1,9 +1,9 @@ class MoneroWalletKeys { const MoneroWalletKeys( - {this.privateSpendKey, - this.privateViewKey, - this.publicSpendKey, - this.publicViewKey}); + {required this.privateSpendKey, + required this.privateViewKey, + required this.publicSpendKey, + required this.publicViewKey}); final String publicViewKey; final String privateViewKey; diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 7de2e84b6..9cf289090 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -1,7 +1,5 @@ import 'dart:io'; - import 'package:cw_core/keyable.dart'; -import 'package:flutter/foundation.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; @@ -11,22 +9,26 @@ import 'package:http/io_client.dart' as ioc; part 'node.g.dart'; Uri createUriFromElectrumAddress(String address) => - Uri.tryParse('tcp://$address'); + Uri.tryParse('tcp://$address')!; @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { Node( - {@required String uri, - @required WalletType type, - this.login, + {this.login, this.password, - this.useSSL}) { - uriRaw = uri; - this.type = type; + this.useSSL, + String? uri, + WalletType? type,}) { + if (uri != null) { + uriRaw = uri; + } + if (type != null) { + this.type = type; + } } - Node.fromMap(Map map) - : uriRaw = map['uri'] as String ?? '', + Node.fromMap(Map map) + : uriRaw = map['uri'] as String? ?? '', login = map['login'] as String, password = map['password'] as String, typeRaw = map['typeRaw'] as int, @@ -36,19 +38,19 @@ class Node extends HiveObject with Keyable { static const boxName = 'Nodes'; @HiveField(0) - String uriRaw; + late String uriRaw; @HiveField(1) - String login; + String? login; @HiveField(2) - String password; + String? password; @HiveField(3) - int typeRaw; + late int typeRaw; @HiveField(4) - bool useSSL; + bool? useSSL; bool get isSSL => useSSL ?? false; @@ -63,7 +65,7 @@ class Node extends HiveObject with Keyable { case WalletType.haven: return Uri.http(uriRaw, ''); default: - return null; + throw Exception('Unexpected type ${type.toString()} for Node uri'); } } @@ -99,7 +101,6 @@ class Node extends HiveObject with Keyable { } Future requestMoneroNode() async { - final path = '/json_rpc'; final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); final realm = 'monero-rpc'; diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index 2c5a2ba5b..e2b1201a8 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -1,20 +1,20 @@ class OutputInfo { const OutputInfo( - {this.fiatAmount, - this.cryptoAmount, - this.address, - this.note, - this.sendAll, - this.extractedAddress, - this.isParsedAddress, - this.formattedCryptoAmount}); + {required this.address, + required this.sendAll, + required this.isParsedAddress, + this.cryptoAmount, + this.formattedCryptoAmount, + this.fiatAmount, + this.note, + this.extractedAddress,}); - final String fiatAmount; - final String cryptoAmount; + final String? fiatAmount; + final String? cryptoAmount; final String address; - final String note; - final String extractedAddress; + final String? note; + final String? extractedAddress; final bool sendAll; final bool isParsedAddress; - final int formattedCryptoAmount; + final int? formattedCryptoAmount; } \ No newline at end of file diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart index bd05892ab..af4838ffa 100644 --- a/cw_core/lib/pathForWallet.dart +++ b/cw_core/lib/pathForWallet.dart @@ -3,7 +3,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; -Future pathForWalletDir({@required String name, @required WalletType type}) async { +Future pathForWalletDir({required String name, required WalletType type}) async { final root = await getApplicationDocumentsDirectory(); final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); @@ -16,11 +16,11 @@ Future pathForWalletDir({@required String name, @required WalletType ty return walletDire.path; } -Future pathForWallet({@required String name, @required WalletType type}) async => +Future pathForWallet({required String name, required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); -Future outdatedAndroidPathForWalletDir({String name}) async { +Future outdatedAndroidPathForWalletDir({required String name}) async { final directory = await getApplicationDocumentsDirectory(); final pathDir = directory.path + '/$name'; diff --git a/cw_core/lib/sec_random_native.dart b/cw_core/lib/sec_random_native.dart index b9800fd71..ce251efc0 100644 --- a/cw_core/lib/sec_random_native.dart +++ b/cw_core/lib/sec_random_native.dart @@ -6,7 +6,7 @@ const utils = const MethodChannel('com.cake_wallet/native_utils'); Future secRandom(int count) async { try { - return await utils.invokeMethod('sec_random', {'count': count}); + return await utils.invokeMethod('sec_random', {'count': count}) ?? Uint8List.fromList([]); } on PlatformException catch (_) { return Uint8List.fromList([]); } diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart index 0f884dfa4..8571544a9 100644 --- a/cw_core/lib/subaddress.dart +++ b/cw_core/lib/subaddress.dart @@ -1,7 +1,7 @@ class Subaddress { - Subaddress({this.id, this.address, this.label}); + Subaddress({required this.id, required this.address, required this.label}); - Subaddress.fromMap(Map map) + Subaddress.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.address = (map['address'] ?? '') as String, this.label = (map['label'] ?? '') as String; diff --git a/cw_core/lib/transaction_direction.dart b/cw_core/lib/transaction_direction.dart index a82420e2c..8d1ddfe02 100644 --- a/cw_core/lib/transaction_direction.dart +++ b/cw_core/lib/transaction_direction.dart @@ -2,16 +2,22 @@ enum TransactionDirection { incoming, outgoing } TransactionDirection parseTransactionDirectionFromInt(int raw) { switch (raw) { - case 0: return TransactionDirection.incoming; - case 1: return TransactionDirection.outgoing; - default: return null; + case 0: + return TransactionDirection.incoming; + case 1: + return TransactionDirection.outgoing; + default: + throw Exception('Unexpected token: raw for TransactionDirection parseTransactionDirectionFromInt'); } } TransactionDirection parseTransactionDirectionFromNumber(String raw) { switch (raw) { - case "0": return TransactionDirection.incoming; - case "1": return TransactionDirection.outgoing; - default: return null; + case "0": + return TransactionDirection.incoming; + case "1": + return TransactionDirection.outgoing; + default: + throw Exception('Unexpected token: raw for TransactionDirection parseTransactionDirectionFromNumber'); } } \ No newline at end of file diff --git a/cw_core/lib/transaction_history.dart b/cw_core/lib/transaction_history.dart index 0011cd811..508f3aeca 100644 --- a/cw_core/lib/transaction_history.dart +++ b/cw_core/lib/transaction_history.dart @@ -1,10 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_info.dart'; abstract class TransactionHistoryBase { - TransactionHistoryBase(); - // : _isUpdating = false; + TransactionHistoryBase() + : transactions = ObservableMap(); @observable ObservableMap transactions; diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index ae6bddbe2..b8e4a5e0c 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -2,21 +2,21 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/keyable.dart'; abstract class TransactionInfo extends Object with Keyable { - String id; - int amount; - int fee; - TransactionDirection direction; - bool isPending; - DateTime date; - int height; - int confirmations; + late String id; + late int amount; + int? fee; + late TransactionDirection direction; + late bool isPending; + late DateTime date; + late int height; + late int confirmations; String amountFormatted(); String fiatAmount(); - String feeFormatted(); + String? feeFormatted(); void changeFiatAmount(String amount); @override dynamic get keyIndex => id; - Map additionalInfo; + late Map additionalInfo; } \ No newline at end of file diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index fe34bf629..c173f1ddd 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -2,5 +2,5 @@ import 'package:cw_core/enumerable_item.dart'; abstract class TransactionPriority extends EnumerableItem with Serializable { - const TransactionPriority({String title, int raw}) : super(title: title, raw: raw); + const TransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); } diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 6ce647dce..3efbe26c5 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -5,11 +5,11 @@ part 'unspent_coins_info.g.dart'; @HiveType(typeId: UnspentCoinsInfo.typeId) class UnspentCoinsInfo extends HiveObject { UnspentCoinsInfo({ - this.walletId, - this.hash, - this.isFrozen, - this.isSending, - this.note}); + required this.walletId, + required this.hash, + required this.isFrozen, + required this.isSending, + required this.note}); static const typeId = 9; static const boxName = 'Unspent'; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 85f9d0b8e..a34101a88 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,9 +1,8 @@ import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { - WalletAddresses(this.walletInfo) { - addressesMap = {}; - } + WalletAddresses(this.walletInfo) + : addressesMap = {}; final WalletInfo walletInfo; @@ -19,10 +18,6 @@ abstract class WalletAddresses { Future saveAddressesInBox() async { try { - if (walletInfo == null) { - return; - } - walletInfo.address = address; walletInfo.addresses = addressesMap; diff --git a/cw_core/lib/wallet_addresses_with_account.dart b/cw_core/lib/wallet_addresses_with_account.dart index 5691ad320..0dcf88de0 100644 --- a/cw_core/lib/wallet_addresses_with_account.dart +++ b/cw_core/lib/wallet_addresses_with_account.dart @@ -5,9 +5,9 @@ import 'package:cw_core/wallet_info.dart'; abstract class WalletAddressesWithAccount extends WalletAddresses { WalletAddressesWithAccount(WalletInfo walletInfo) : super(walletInfo); - T get account; + // T get account; - set account(T account); + // set account(T account); AccountList get accountList; } \ No newline at end of file diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 173ba8155..1983e62b7 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -1,12 +1,12 @@ import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -48,15 +48,15 @@ abstract class WalletBase< WalletAddresses get walletAddresses; - HistoryType transactionHistory; + late HistoryType transactionHistory; - Future connectToNode({@required Node node}); + Future connectToNode({required Node node}); Future startSync(); Future createTransaction(Object credentials); - int calculateEstimatedFee(TransactionPriority priority, int amount); + int calculateEstimatedFee(TransactionPriority priority, int? amount); // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, @@ -66,7 +66,7 @@ abstract class WalletBase< Future save(); - Future rescan({int height}); + Future rescan({required int height}); void close(); diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 69c07bfda..e028232e8 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -1,10 +1,14 @@ import 'package:cw_core/wallet_info.dart'; abstract class WalletCredentials { - WalletCredentials({this.name, this.password, this.height, this.walletInfo}); + WalletCredentials({ + required this.name, + this.height, + this.walletInfo, + this.password}); final String name; - final int height; - String password; - WalletInfo walletInfo; + final int? height; + String? password; + WalletInfo? walletInfo; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 4e614811e..130b8ff5f 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -13,20 +13,20 @@ class WalletInfo extends HiveObject { : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external( - {@required String id, - @required String name, - @required WalletType type, - @required bool isRecovery, - @required int restoreHeight, - @required DateTime date, - @required String dirPath, - @required String path, - @required String address, + {required String id, + required String name, + required WalletType type, + required bool isRecovery, + required int restoreHeight, + required DateTime date, + required String dirPath, + required String path, + required String address, + bool? showIntroCakePayCard, String yatEid ='', - String yatLastUsedAddressRaw = '', - bool showIntroCakePayCard}) { + String yatLastUsedAddressRaw = ''}) { return WalletInfo(id, name, type, isRecovery, restoreHeight, - date.millisecondsSinceEpoch ?? 0, dirPath, path, address, + date.millisecondsSinceEpoch, dirPath, path, address, yatEid, yatLastUsedAddressRaw, showIntroCakePayCard); } @@ -61,7 +61,7 @@ class WalletInfo extends HiveObject { String address; @HiveField(10) - Map addresses; + Map? addresses; @HiveField(11) String yatEid; @@ -70,7 +70,7 @@ class WalletInfo extends HiveObject { String yatLastUsedAddressRaw; @HiveField(13) - bool showIntroCakePayCard; + bool? showIntroCakePayCard; String get yatLastUsedAddress => yatLastUsedAddressRaw; @@ -85,7 +85,7 @@ class WalletInfo extends HiveObject { if(showIntroCakePayCard == null) { return type != WalletType.haven; } - return showIntroCakePayCard; + return showIntroCakePayCard!; } DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 6a39fa63f..e76e4539e 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -55,7 +55,7 @@ WalletType deserializeFromInt(int raw) { case 3: return WalletType.haven; default: - return null; + throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } } @@ -100,6 +100,6 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { case WalletType.haven: return CryptoCurrency.xhv; default: - return null; + throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 9858511de..951a97ffb 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -7,35 +7,35 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "4.7.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.3.1" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.8.1" + version: "1.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -49,42 +49,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.10" + version: "3.1.0" build_resolvers: dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "7.2.4" built_collection: dependency: transitive description: @@ -105,98 +105,77 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.3.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.2" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.12" - dartx: - dependency: transitive - description: - name: dartx - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" + version: "2.2.4" encrypt: dependency: "direct main" description: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "5.0.1" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" file: dependency: transitive description: @@ -222,12 +201,19 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "1.1.0+2" + version: "2.0.6+4" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" glob: dependency: transitive description: @@ -241,42 +227,42 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.1.0" hive: dependency: transitive description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.4+1" + version: "2.2.3" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "1.1.3" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.1" intl: dependency: "direct main" description: @@ -290,7 +276,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.3" js: dependency: transitive description: @@ -304,7 +290,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.6.0" logging: dependency: transitive description: @@ -318,14 +304,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.8.0" mime: dependency: transitive description: @@ -339,63 +332,77 @@ packages: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "1.2.1+4" + version: "2.1.0" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.7+3" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.28" + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+8" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.5" + version: "2.1.3" pedantic: dependency: transitive description: @@ -416,14 +423,14 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.6.2" pool: dependency: transitive description: @@ -451,21 +458,21 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" + version: "1.2.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -477,14 +484,21 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.2.3" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -512,35 +526,28 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - time: - dependency: transitive - description: - name: time - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.1" + version: "0.4.12" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" typed_data: dependency: transitive description: @@ -554,7 +561,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" watcher: dependency: transitive description: @@ -568,21 +575,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.2.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "3.0.0" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0+2" yaml: dependency: transitive description: @@ -591,5 +598,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=3.0.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 4caa92e11..50503361c 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -6,26 +6,26 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.17.5 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - http: ^0.12.0+2 - path_provider: ^1.3.0 - mobx: ^1.2.1+2 - flutter_mobx: ^1.1.0+2 + http: ^0.13.4 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 intl: ^0.17.0 - encrypt: ^4.0.0 + encrypt: ^5.0.1 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.10.3 - build_resolvers: ^1.3.10 - mobx_codegen: ^1.1.0+1 - hive_generator: ^0.8.1 + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_haven/lib/api/account_list.dart b/cw_haven/lib/api/account_list.dart index 96bf3d654..a05446c8e 100644 --- a/cw_haven/lib/api/account_list.dart +++ b/cw_haven/lib/api/account_list.dart @@ -50,16 +50,16 @@ List getAllAccount() { .toList(); } -void addAccountSync({String label}) { - final labelPointer = Utf8.toUtf8(label); +void addAccountSync({required String label}) { + final labelPointer = label.toNativeUtf8(); accountAddNewNative(labelPointer); - free(labelPointer); + calloc.free(labelPointer); } -void setLabelForAccountSync({int accountIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); +void setLabelForAccountSync({required int accountIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); accountSetLabelNative(accountIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void _addAccount(String label) => addAccountSync(label: label); @@ -71,12 +71,12 @@ void _setLabelForAccount(Map args) { setLabelForAccountSync(label: label, accountIndex: accountIndex); } -Future addAccount({String label}) async { +Future addAccount({required String label}) async { await compute(_addAccount, label); await store(); } -Future setLabelForAccount({int accountIndex, String label}) async { +Future setLabelForAccount({required int accountIndex, required String label}) async { await compute( _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); await store(); diff --git a/cw_haven/lib/api/convert_utf8_to_string.dart b/cw_haven/lib/api/convert_utf8_to_string.dart index 7fa5a68df..41a6b648a 100644 --- a/cw_haven/lib/api/convert_utf8_to_string.dart +++ b/cw_haven/lib/api/convert_utf8_to_string.dart @@ -1,8 +1,8 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; -String convertUTF8ToString({Pointer pointer}) { - final str = Utf8.fromUtf8(pointer); - free(pointer); +String convertUTF8ToString({required Pointer pointer}) { + final str = pointer.toDartString(); + calloc.free(pointer); return str; } \ No newline at end of file diff --git a/cw_haven/lib/api/cw_haven.dart b/cw_haven/lib/api/cw_haven.dart index 1d3726e17..0e48276d9 100644 --- a/cw_haven/lib/api/cw_haven.dart +++ b/cw_haven/lib/api/cw_haven.dart @@ -8,7 +8,7 @@ class CwHaven { const MethodChannel('cw_haven'); static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); + final String version = await _channel.invokeMethod('getPlatformVersion') ?? ''; return version; } } diff --git a/cw_haven/lib/api/exceptions/connection_to_node_exception.dart b/cw_haven/lib/api/exceptions/connection_to_node_exception.dart index 6ee272b89..483b0a174 100644 --- a/cw_haven/lib/api/exceptions/connection_to_node_exception.dart +++ b/cw_haven/lib/api/exceptions/connection_to_node_exception.dart @@ -1,5 +1,5 @@ class ConnectionToNodeException implements Exception { - ConnectionToNodeException({this.message}); + ConnectionToNodeException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/creation_transaction_exception.dart b/cw_haven/lib/api/exceptions/creation_transaction_exception.dart index bb477d673..7b55ec074 100644 --- a/cw_haven/lib/api/exceptions/creation_transaction_exception.dart +++ b/cw_haven/lib/api/exceptions/creation_transaction_exception.dart @@ -1,5 +1,5 @@ class CreationTransactionException implements Exception { - CreationTransactionException({this.message}); + CreationTransactionException({required this.message}); final String message; diff --git a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart index ce43c0ec6..b6e0c1f18 100644 --- a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart @@ -1,5 +1,5 @@ class SetupWalletException implements Exception { - SetupWalletException({this.message}); + SetupWalletException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_creation_exception.dart b/cw_haven/lib/api/exceptions/wallet_creation_exception.dart index 6b00445ad..6052366b9 100644 --- a/cw_haven/lib/api/exceptions/wallet_creation_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_creation_exception.dart @@ -1,5 +1,5 @@ class WalletCreationException implements Exception { - WalletCreationException({this.message}); + WalletCreationException({required this.message}); final String message; diff --git a/cw_haven/lib/api/exceptions/wallet_opening_exception.dart b/cw_haven/lib/api/exceptions/wallet_opening_exception.dart index 8d84b0f7e..df7a850a4 100644 --- a/cw_haven/lib/api/exceptions/wallet_opening_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_opening_exception.dart @@ -1,5 +1,5 @@ class WalletOpeningException implements Exception { - WalletOpeningException({this.message}); + WalletOpeningException({required this.message}); final String message; diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart index 5f08437d4..c6b6c6ef7 100644 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -1,5 +1,5 @@ class WalletRestoreFromKeysException implements Exception { - WalletRestoreFromKeysException({this.message}); + WalletRestoreFromKeysException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart index fd89e4161..004cd7958 100644 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -1,5 +1,5 @@ class WalletRestoreFromSeedException implements Exception { - WalletRestoreFromSeedException({this.message}); + WalletRestoreFromSeedException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_haven/lib/api/monero_output.dart b/cw_haven/lib/api/monero_output.dart index 831ee1f22..a6d735bd3 100644 --- a/cw_haven/lib/api/monero_output.dart +++ b/cw_haven/lib/api/monero_output.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; class MoneroOutput { - MoneroOutput({@required this.address, @required this.amount}); + MoneroOutput({required this.address, required this.amount}); final String address; final String amount; diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart index 9dd1c8dac..774a303d7 100644 --- a/cw_haven/lib/api/signatures.dart +++ b/cw_haven/lib/api/signatures.dart @@ -39,7 +39,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer, Pointer, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); typedef start_refresh = Void Function(); @@ -86,7 +86,7 @@ typedef account_set_label = Void Function( typedef transactions_refresh = Void Function(); -typedef get_tx_key = Pointer Function(Pointer txId); +typedef get_tx_key = Pointer? Function(Pointer txId); typedef transactions_count = Int64 Function(); diff --git a/cw_haven/lib/api/structs/account_row.dart b/cw_haven/lib/api/structs/account_row.dart index c3fc22de1..aa492ee0f 100644 --- a/cw_haven/lib/api/structs/account_row.dart +++ b/cw_haven/lib/api/structs/account_row.dart @@ -3,9 +3,10 @@ import 'package:ffi/ffi.dart'; class AccountRow extends Struct { @Int64() - int id; - Pointer label; + external int id; + + external Pointer label; - String getLabel() => Utf8.fromUtf8(label); + String getLabel() => label.toDartString(); int getId() => id; } diff --git a/cw_haven/lib/api/structs/haven_balance_row.dart b/cw_haven/lib/api/structs/haven_balance_row.dart index 4b68a7bb6..b0f657bca 100644 --- a/cw_haven/lib/api/structs/haven_balance_row.dart +++ b/cw_haven/lib/api/structs/haven_balance_row.dart @@ -3,9 +3,10 @@ import 'package:ffi/ffi.dart'; class HavenBalanceRow extends Struct { @Int64() - int amount; - Pointer assetType; + external int amount; + + external Pointer assetType; int getAmount() => amount; - String getAssetType() => Utf8.fromUtf8(assetType); + String getAssetType() => assetType.toDartString(); } diff --git a/cw_haven/lib/api/structs/haven_rate.dart b/cw_haven/lib/api/structs/haven_rate.dart index 818615559..48f188135 100644 --- a/cw_haven/lib/api/structs/haven_rate.dart +++ b/cw_haven/lib/api/structs/haven_rate.dart @@ -3,9 +3,10 @@ import 'package:ffi/ffi.dart'; class HavenRate extends Struct { @Int64() - int rate; - Pointer assetType; + external int rate; + + external Pointer assetType; int getRate() => rate; - String getAssetType() => Utf8.fromUtf8(assetType); + String getAssetType() => assetType.toDartString(); } diff --git a/cw_haven/lib/api/structs/pending_transaction.dart b/cw_haven/lib/api/structs/pending_transaction.dart index b492f28a0..12e5233f1 100644 --- a/cw_haven/lib/api/structs/pending_transaction.dart +++ b/cw_haven/lib/api/structs/pending_transaction.dart @@ -3,18 +3,22 @@ import 'package:ffi/ffi.dart'; class PendingTransactionRaw extends Struct { @Int64() - int amount; + external int amount; @Int64() - int fee; + external int fee; - Pointer hash; + external Pointer hash; - String getHash() => Utf8.fromUtf8(hash); + String getHash() => hash.toDartString(); } class PendingTransactionDescription { - PendingTransactionDescription({this.amount, this.fee, this.hash, this.pointerAddress}); + PendingTransactionDescription({ + required this.amount, + required this.fee, + required this.hash, + required this.pointerAddress}); final int amount; final int fee; diff --git a/cw_haven/lib/api/structs/subaddress_row.dart b/cw_haven/lib/api/structs/subaddress_row.dart index 1673e00c7..d593a793d 100644 --- a/cw_haven/lib/api/structs/subaddress_row.dart +++ b/cw_haven/lib/api/structs/subaddress_row.dart @@ -3,11 +3,13 @@ import 'package:ffi/ffi.dart'; class SubaddressRow extends Struct { @Int64() - int id; - Pointer address; - Pointer label; + external int id; + + external Pointer address; + + external Pointer label; - String getLabel() => Utf8.fromUtf8(label); - String getAddress() => Utf8.fromUtf8(address); + String getLabel() => label.toDartString(); + String getAddress() => address.toDartString(); int getId() => id; } \ No newline at end of file diff --git a/cw_haven/lib/api/structs/transaction_info_row.dart b/cw_haven/lib/api/structs/transaction_info_row.dart index 68a84e0a2..177cdfde7 100644 --- a/cw_haven/lib/api/structs/transaction_info_row.dart +++ b/cw_haven/lib/api/structs/transaction_info_row.dart @@ -3,42 +3,42 @@ import 'package:ffi/ffi.dart'; class TransactionInfoRow extends Struct { @Uint64() - int amount; + external int amount; @Uint64() - int fee; + external int fee; @Uint64() - int blockHeight; + external int blockHeight; @Uint64() - int confirmations; + external int confirmations; @Uint32() - int subaddrAccount; + external int subaddrAccount; @Int8() - int direction; + external int direction; @Int8() - int isPending; + external int isPending; @Uint32() - int subaddrIndex; + external int subaddrIndex; - Pointer hash; + external Pointer hash; - Pointer paymentId; + external Pointer paymentId; - Pointer assetType; + external Pointer assetType; @Int64() - int datetime; + external int datetime; int getDatetime() => datetime; int getAmount() => amount >= 0 ? amount : amount * -1; bool getIsPending() => isPending != 0; - String getHash() => Utf8.fromUtf8(hash); - String getPaymentId() => Utf8.fromUtf8(paymentId); - String getAssetType() => Utf8.fromUtf8(assetType); + String getHash() => hash.toDartString(); + String getPaymentId() => paymentId.toDartString(); + String getAssetType() => assetType.toDartString(); } diff --git a/cw_haven/lib/api/structs/ut8_box.dart b/cw_haven/lib/api/structs/ut8_box.dart index a6f41bc75..53e678c88 100644 --- a/cw_haven/lib/api/structs/ut8_box.dart +++ b/cw_haven/lib/api/structs/ut8_box.dart @@ -2,7 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; class Utf8Box extends Struct { - Pointer value; + external Pointer value; - String getValue() => Utf8.fromUtf8(value); + String getValue() => value.toDartString(); } diff --git a/cw_haven/lib/api/subaddress_list.dart b/cw_haven/lib/api/subaddress_list.dart index c735400ec..39dbeab78 100644 --- a/cw_haven/lib/api/subaddress_list.dart +++ b/cw_haven/lib/api/subaddress_list.dart @@ -29,7 +29,7 @@ final subaddrressSetLabelNative = havenApi bool isUpdating = false; -void refreshSubaddresses({@required int accountIndex}) { +void refreshSubaddresses({required int accountIndex}) { try { isUpdating = true; subaddressRefreshNative(accountIndex); @@ -50,18 +50,18 @@ List getAllSubaddresses() { .toList(); } -void addSubaddressSync({int accountIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); +void addSubaddressSync({required int accountIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); subaddrressAddNewNative(accountIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void setLabelForSubaddressSync( - {int accountIndex, int addressIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); + {required int accountIndex, required int addressIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); subaddrressSetLabelNative(accountIndex, addressIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void _addSubaddress(Map args) { @@ -80,14 +80,14 @@ void _setLabelForSubaddress(Map args) { accountIndex: accountIndex, addressIndex: addressIndex, label: label); } -Future addSubaddress({int accountIndex, String label}) async { +Future addSubaddress({required int accountIndex, required String label}) async { await compute, void>( _addSubaddress, {'accountIndex': accountIndex, 'label': label}); await store(); } Future setLabelForSubaddress( - {int accountIndex, int addressIndex, String label}) async { + {required int accountIndex, required int addressIndex, required String label}) async { await compute, void>(_setLabelForSubaddress, { 'accountIndex': accountIndex, 'addressIndex': addressIndex, diff --git a/cw_haven/lib/api/transaction_history.dart b/cw_haven/lib/api/transaction_history.dart index 185207941..f658133e1 100644 --- a/cw_haven/lib/api/transaction_history.dart +++ b/cw_haven/lib/api/transaction_history.dart @@ -40,16 +40,16 @@ final getTxKeyNative = havenApi .asFunction(); String getTxKey(String txId) { - final txIdPointer = Utf8.toUtf8(txId); + final txIdPointer = txId.toNativeUtf8(); final keyPointer = getTxKeyNative(txIdPointer); - free(txIdPointer); + calloc.free(txIdPointer); if (keyPointer != null) { return convertUTF8ToString(pointer: keyPointer); } - return null; + return ''; } void refreshTransactions() => transactionsRefreshNative(); @@ -67,18 +67,18 @@ List getAllTransations() { } PendingTransactionDescription createTransactionSync( - {String address, - String assetType, - String paymentId, - String amount, - int priorityRaw, + {required String address, + required String assetType, + required String paymentId, + required int priorityRaw, + String? amount, int accountIndex = 0}) { - final addressPointer = Utf8.toUtf8(address); - final assetTypePointer = Utf8.toUtf8(assetType); - final paymentIdPointer = Utf8.toUtf8(paymentId); - final amountPointer = amount != null ? Utf8.toUtf8(amount) : nullptr; - final errorMessagePointer = allocate(); - final pendingTransactionRawPointer = allocate(); + final addressPointer = address.toNativeUtf8(); + final assetTypePointer = assetType.toNativeUtf8(); + final paymentIdPointer = paymentId.toNativeUtf8(); + final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + final errorMessagePointer = calloc(); + final pendingTransactionRawPointer = calloc(); final created = transactionCreateNative( addressPointer, assetTypePointer, @@ -90,17 +90,17 @@ PendingTransactionDescription createTransactionSync( pendingTransactionRawPointer) != 0; - free(addressPointer); - free(assetTypePointer); - free(paymentIdPointer); + calloc.free(addressPointer); + calloc.free(assetTypePointer); + calloc.free(paymentIdPointer); if (amountPointer != nullptr) { - free(amountPointer); + calloc.free(amountPointer); } if (!created) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } @@ -112,28 +112,28 @@ PendingTransactionDescription createTransactionSync( } PendingTransactionDescription createTransactionMultDestSync( - {List outputs, - String assetType, - String paymentId, - int priorityRaw, + {required List outputs, + required String assetType, + required String paymentId, + required int priorityRaw, int accountIndex = 0}) { final int size = outputs.length; final List> addressesPointers = outputs.map((output) => - Utf8.toUtf8(output.address)).toList(); - final Pointer> addressesPointerPointer = allocate(count: size); + output.address.toNativeUtf8()).toList(); + final Pointer> addressesPointerPointer = calloc(size); final List> amountsPointers = outputs.map((output) => - Utf8.toUtf8(output.amount)).toList(); - final Pointer> amountsPointerPointer = allocate(count: size); + output.amount.toNativeUtf8()).toList(); + final Pointer> amountsPointerPointer = calloc( size); for (int i = 0; i < size; i++) { addressesPointerPointer[i] = addressesPointers[i]; amountsPointerPointer[i] = amountsPointers[i]; } - final assetTypePointer = Utf8.toUtf8(assetType); - final paymentIdPointer = Utf8.toUtf8(paymentId); - final errorMessagePointer = allocate(); - final pendingTransactionRawPointer = allocate(); + final assetTypePointer = assetType.toNativeUtf8(); + final paymentIdPointer = paymentId.toNativeUtf8(); + final errorMessagePointer = calloc(); + final pendingTransactionRawPointer = calloc(); final created = transactionCreateMultDestNative( addressesPointerPointer, assetTypePointer, @@ -146,18 +146,18 @@ PendingTransactionDescription createTransactionMultDestSync( pendingTransactionRawPointer) != 0; - free(addressesPointerPointer); - free(assetTypePointer); - free(amountsPointerPointer); + calloc.free(addressesPointerPointer); + calloc.free(assetTypePointer); + calloc.free(amountsPointerPointer); - addressesPointers.forEach((element) => free(element)); - amountsPointers.forEach((element) => free(element)); + addressesPointers.forEach((element) => calloc.free(element)); + amountsPointers.forEach((element) => calloc.free(element)); - free(paymentIdPointer); + calloc.free(paymentIdPointer); if (!created) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } @@ -168,17 +168,17 @@ PendingTransactionDescription createTransactionMultDestSync( pointerAddress: pendingTransactionRawPointer.address); } -void commitTransactionFromPointerAddress({int address}) => commitTransaction( +void commitTransactionFromPointerAddress({required int address}) => commitTransaction( transactionPointer: Pointer.fromAddress(address)); -void commitTransaction({Pointer transactionPointer}) { - final errorMessagePointer = allocate(); +void commitTransaction({required Pointer transactionPointer}) { + final errorMessagePointer = calloc(); final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; if (!isCommited) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } } @@ -216,11 +216,11 @@ PendingTransactionDescription _createTransactionMultDestSync(Map args) { } Future createTransaction( - {String address, - String assetType, + {required String address, + required String assetType, + required int priorityRaw, + String? amount, String paymentId = '', - String amount, - int priorityRaw, int accountIndex = 0}) => compute(_createTransactionSync, { 'address': address, @@ -232,10 +232,10 @@ Future createTransaction( }); Future createTransactionMultDest( - {List outputs, - String assetType, + {required List outputs, + required int priorityRaw, + String? assetType, String paymentId = '', - int priorityRaw, int accountIndex = 0}) => compute(_createTransactionMultDestSync, { 'outputs': outputs, diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart index 878297501..f1dc0e26b 100644 --- a/cw_haven/lib/api/types.dart +++ b/cw_haven/lib/api/types.dart @@ -39,7 +39,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer, Pointer, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer); typedef StartRefresh = void Function(); @@ -84,7 +84,7 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer label); typedef TransactionsRefresh = void Function(); -typedef GetTxKey = Pointer Function(Pointer txId); +typedef GetTxKey = Pointer? Function(Pointer txId); typedef TransactionsCount = int Function(); diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart index 3370fd3e0..2b85f60e2 100644 --- a/cw_haven/lib/api/wallet.dart +++ b/cw_haven/lib/api/wallet.dart @@ -142,24 +142,24 @@ int getNodeHeightSync() => getNodeHeightNative(); bool isConnectedSync() => isConnectedNative() != 0; bool setupNodeSync( - {String address, - String login, - String password, + {required String address, + String? login, + String? password, bool useSSL = false, bool isLightWallet = false}) { - final addressPointer = Utf8.toUtf8(address); - Pointer loginPointer; - Pointer passwordPointer; + final addressPointer = address.toNativeUtf8(); + Pointer? loginPointer; + Pointer? passwordPointer; if (login != null) { - loginPointer = Utf8.toUtf8(login); + loginPointer = login.toNativeUtf8(); } if (password != null) { - passwordPointer = Utf8.toUtf8(password); + passwordPointer = password.toNativeUtf8(); } - final errorMessagePointer = allocate(); + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, loginPointer, @@ -169,9 +169,15 @@ bool setupNodeSync( errorMessagePointer) != 0; - free(addressPointer); - free(loginPointer); - free(passwordPointer); + calloc.free(addressPointer); + + if (loginPointer != null) { + calloc.free(loginPointer); + } + + if (passwordPointer != null) { + calloc.free(passwordPointer); + } if (!isSetupNode) { throw SetupWalletException( @@ -185,31 +191,31 @@ void startRefreshSync() => startRefreshNative(); Future connectToNode() async => connecToNodeNative() != 0; -void setRefreshFromBlockHeight({int height}) => +void setRefreshFromBlockHeight({required int height}) => setRefreshFromBlockHeightNative(height); -void setRecoveringFromSeed({bool isRecovery}) => +void setRecoveringFromSeed({required bool isRecovery}) => setRecoveringFromSeedNative(_boolToInt(isRecovery)); void storeSync() { - final pathPointer = Utf8.toUtf8(''); + final pathPointer = ''.toNativeUtf8(); storeNative(pathPointer); - free(pathPointer); + calloc.free(pathPointer); } void setPasswordSync(String password) { - final passwordPointer = Utf8.toUtf8(password); - final errorMessagePointer = allocate(); + final passwordPointer = password.toNativeUtf8(); + final errorMessagePointer = calloc(); final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; - free(passwordPointer); + calloc.free(passwordPointer); if (!changed) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw Exception(message); } - free(errorMessagePointer); + calloc.free(errorMessagePointer); } void closeCurrentWallet() => closeCurrentWalletNative(); @@ -227,16 +233,15 @@ String getPublicSpendKey() => convertUTF8ToString(pointer: getPublicSpendKeyNative()); class SyncListener { - SyncListener(this.onNewBlock, this.onNewTransaction) { - _cachedBlockchainHeight = 0; - _lastKnownBlockHeight = 0; - _initialSyncHeight = 0; - } + SyncListener(this.onNewBlock, this.onNewTransaction) + : _cachedBlockchainHeight = 0, + _lastKnownBlockHeight = 0, + _initialSyncHeight = 0; void Function(int, int, double) onNewBlock; void Function() onNewTransaction; - Timer _updateSyncInfoTimer; + Timer? _updateSyncInfoTimer; int _cachedBlockchainHeight; int _lastKnownBlockHeight; int _initialSyncHeight; @@ -325,13 +330,13 @@ int _getNodeHeight(Object _) => getNodeHeightSync(); void startRefresh() => startRefreshSync(); -Future setupNode( - {String address, - String login, - String password, +Future setupNode( + {required String address, + String? login, + String? password, bool useSSL = false, bool isLightWallet = false}) => - compute, void>(_setupNodeSync, { + compute, void>(_setupNodeSync, { 'address': address, 'login': login, 'password': password, @@ -339,7 +344,7 @@ Future setupNode( 'isLightWallet': isLightWallet }); -Future store() => compute(_storeSync, 0); +Future store() => compute(_storeSync, 0); Future isConnected() => compute(_isConnected, 0); diff --git a/cw_haven/lib/api/wallet_manager.dart b/cw_haven/lib/api/wallet_manager.dart index d134dcbb4..627fc226a 100644 --- a/cw_haven/lib/api/wallet_manager.dart +++ b/cw_haven/lib/api/wallet_manager.dart @@ -38,18 +38,18 @@ final errorStringNative = havenApi .asFunction(); void createWalletSync( - {String path, String password, String language, int nettype = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final languagePointer = Utf8.toUtf8(language); - final errorMessagePointer = allocate(); + {required String path, required String password, required String language, int nettype = 0}) { + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final languagePointer = language.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletCreated = createWalletNative(pathPointer, passwordPointer, languagePointer, nettype, errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(languagePointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(languagePointer); if (!isWalletCreated) { throw WalletCreationException( @@ -59,25 +59,25 @@ void createWalletSync( // setupNodeSync(address: "node.moneroworld.com:18089"); } -bool isWalletExistSync({String path}) { - final pathPointer = Utf8.toUtf8(path); +bool isWalletExistSync({required String path}) { + final pathPointer = path.toNativeUtf8(); final isExist = isWalletExistNative(pathPointer) != 0; - free(pathPointer); + calloc.free(pathPointer); return isExist; } void restoreWalletFromSeedSync( - {String path, - String password, - String seed, + {required String path, + required String password, + required String seed, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final seedPointer = Utf8.toUtf8(seed); - final errorMessagePointer = allocate(); + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final seedPointer = seed.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletRestored = restoreWalletFromSeedNative( pathPointer, passwordPointer, @@ -87,9 +87,9 @@ void restoreWalletFromSeedSync( errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(seedPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(seedPointer); if (!isWalletRestored) { throw WalletRestoreFromSeedException( @@ -98,21 +98,21 @@ void restoreWalletFromSeedSync( } void restoreWalletFromKeysSync( - {String path, - String password, - String language, - String address, - String viewKey, - String spendKey, + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final languagePointer = Utf8.toUtf8(language); - final addressPointer = Utf8.toUtf8(address); - final viewKeyPointer = Utf8.toUtf8(viewKey); - final spendKeyPointer = Utf8.toUtf8(spendKey); - final errorMessagePointer = allocate(); + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final languagePointer = language.toNativeUtf8(); + final addressPointer = address.toNativeUtf8(); + final viewKeyPointer = viewKey.toNativeUtf8(); + final spendKeyPointer = spendKey.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletRestored = restoreWalletFromKeysNative( pathPointer, passwordPointer, @@ -125,12 +125,12 @@ void restoreWalletFromKeysSync( errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(languagePointer); - free(addressPointer); - free(viewKeyPointer); - free(spendKeyPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(languagePointer); + calloc.free(addressPointer); + calloc.free(viewKeyPointer); + calloc.free(spendKeyPointer); if (!isWalletRestored) { throw WalletRestoreFromKeysException( @@ -138,12 +138,12 @@ void restoreWalletFromKeysSync( } } -void loadWallet({String path, String password, int nettype = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); +void loadWallet({required String path, required String password, int nettype = 0}) { + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); final loaded = loadWalletNative(pathPointer, passwordPointer, nettype) != 0; - free(pathPointer); - free(passwordPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); if (!loaded) { throw WalletOpeningException( @@ -189,20 +189,20 @@ void _restoreFromKeys(Map args) { } Future _openWallet(Map args) async => - loadWallet(path: args['path'], password: args['password']); + loadWallet(path: args['path'] as String, password: args['password'] as String); bool _isWalletExist(String path) => isWalletExistSync(path: path); -void openWallet({String path, String password, int nettype = 0}) async => +void openWallet({required String path, required String password, int nettype = 0}) async => loadWallet(path: path, password: password, nettype: nettype); Future openWalletAsync(Map args) async => compute(_openWallet, args); Future createWallet( - {String path, - String password, - String language, + {required String path, + required String password, + required String language, int nettype = 0}) async => compute(_createWallet, { 'path': path, @@ -211,10 +211,10 @@ Future createWallet( 'nettype': nettype }); -Future restoreFromSeed( - {String path, - String password, - String seed, +Future restoreFromSeed( + {required String path, + required String password, + required String seed, int nettype = 0, int restoreHeight = 0}) async => compute, void>(_restoreFromSeed, { @@ -225,13 +225,13 @@ Future restoreFromSeed( 'restoreHeight': restoreHeight }); -Future restoreFromKeys( - {String path, - String password, - String language, - String address, - String viewKey, - String spendKey, +Future restoreFromKeys( + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, int nettype = 0, int restoreHeight = 0}) async => compute, void>(_restoreFromKeys, { @@ -245,4 +245,4 @@ Future restoreFromKeys( 'restoreHeight': restoreHeight }); -Future isWalletExist({String path}) => compute(_isWalletExist, path); +Future isWalletExist({required String path}) => compute(_isWalletExist, path); diff --git a/cw_haven/lib/haven_account_list.dart b/cw_haven/lib/haven_account_list.dart index ad1cd2a7b..9399efc27 100644 --- a/cw_haven/lib/haven_account_list.dart +++ b/cw_haven/lib/haven_account_list.dart @@ -53,13 +53,13 @@ abstract class HavenAccountListBase extends AccountList with Store { .toList(); @override - Future addAccount({String label}) async { + Future addAccount({required String label}) async { await account_list.addAccount(label: label); update(); } @override - Future setLabelAccount({int accountIndex, String label}) async { + Future setLabelAccount({required int accountIndex, required String label}) async { await account_list.setLabelForAccount( accountIndex: accountIndex, label: label); update(); diff --git a/cw_haven/lib/haven_balance.dart b/cw_haven/lib/haven_balance.dart index 17172fa9c..7d257ded9 100644 --- a/cw_haven/lib/haven_balance.dart +++ b/cw_haven/lib/haven_balance.dart @@ -9,7 +9,7 @@ const inactiveBalances = [ CryptoCurrency.xnok, CryptoCurrency.xnzd]; -Map getHavenBalance({int accountIndex}) { +Map getHavenBalance({required int accountIndex}) { final fullBalances = getHavenFullBalance(accountIndex: accountIndex); final unlockedBalances = getHavenUnlockedBalance(accountIndex: accountIndex); final havenBalances = {}; diff --git a/cw_haven/lib/haven_subaddress_list.dart b/cw_haven/lib/haven_subaddress_list.dart index b6213d784..b40b3484c 100644 --- a/cw_haven/lib/haven_subaddress_list.dart +++ b/cw_haven/lib/haven_subaddress_list.dart @@ -10,11 +10,10 @@ class HavenSubaddressList = HavenSubaddressListBase with _$HavenSubaddressList; abstract class HavenSubaddressListBase with Store { - HavenSubaddressListBase() { - _isRefreshing = false; - _isUpdating = false; + HavenSubaddressListBase() + : _isRefreshing = false, + _isUpdating = false, subaddresses = ObservableList(); - } @observable ObservableList subaddresses; @@ -22,7 +21,7 @@ abstract class HavenSubaddressListBase with Store { bool _isRefreshing; bool _isUpdating; - void update({int accountIndex}) { + void update({required int accountIndex}) { if (_isUpdating) { return; } @@ -56,20 +55,20 @@ abstract class HavenSubaddressListBase with Store { .toList(); } - Future addSubaddress({int accountIndex, String label}) async { + Future addSubaddress({required int accountIndex, required String label}) async { await subaddress_list.addSubaddress( accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } - Future setLabelSubaddress( - {int accountIndex, int addressIndex, String label}) async { + Future setLabelSubaddress( + {required int accountIndex, required int addressIndex, required String label}) async { await subaddress_list.setLabelForSubaddress( accountIndex: accountIndex, addressIndex: addressIndex, label: label); update(accountIndex: accountIndex); } - void refresh({int accountIndex}) { + void refresh({required int accountIndex}) { if (_isRefreshing) { return; } diff --git a/cw_haven/lib/haven_transaction_creation_credentials.dart b/cw_haven/lib/haven_transaction_creation_credentials.dart index 88525f0fd..4de41a504 100644 --- a/cw_haven/lib/haven_transaction_creation_credentials.dart +++ b/cw_haven/lib/haven_transaction_creation_credentials.dart @@ -2,7 +2,10 @@ import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class HavenTransactionCreationCredentials { - HavenTransactionCreationCredentials({this.outputs, this.priority, this.assetType}); + HavenTransactionCreationCredentials({ + required this.outputs, + required this.priority, + required this.assetType}); final List outputs; final MoneroTransactionPriority priority; diff --git a/cw_haven/lib/haven_transaction_info.dart b/cw_haven/lib/haven_transaction_info.dart index 16f032541..277370467 100644 --- a/cw_haven/lib/haven_transaction_info.dart +++ b/cw_haven/lib/haven_transaction_info.dart @@ -10,20 +10,20 @@ class HavenTransactionInfo extends TransactionInfo { HavenTransactionInfo(this.id, this.height, this.direction, this.date, this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee); - HavenTransactionInfo.fromMap(Map map) + HavenTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, direction = parseTransactionDirectionFromNumber(map['direction'] as String) ?? TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + int.parse(map['timestamp'] as String? ?? '0') * 1000), isPending = parseBoolFromString(map['isPending'] as String), amount = map['amount'] as int, accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int ?? 0; + fee = map['fee'] as int? ?? 0; HavenTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), @@ -48,11 +48,10 @@ class HavenTransactionInfo extends TransactionInfo { final int amount; final int fee; final int addressIndex; - String recipientAddress; - String key; - String assetType; - - String _fiatAmount; + late String recipientAddress; + late String assetType; + String? _fiatAmount; + String? key; @override String amountFormatted() => diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index c107d2f52..0e33753d5 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -37,15 +37,19 @@ class HavenWallet = HavenWalletBase with _$HavenWallet; abstract class HavenWalletBase extends WalletBase with Store { - HavenWalletBase({WalletInfo walletInfo}) - : super(walletInfo) { + HavenWalletBase({required WalletInfo walletInfo}) + : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), + _isTransactionUpdating = false, + _hasSyncAfterStartup = false, + walletAddresses = HavenWalletAddresses(walletInfo), + syncStatus = NotConnectedSyncStatus(), + super(walletInfo) { transactionHistory = HavenTransactionHistory(); - balance = ObservableMap.of(getHavenBalance(accountIndex: 0)); - _isTransactionUpdating = false; - _hasSyncAfterStartup = false; - walletAddresses = HavenWalletAddresses(walletInfo); _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account account) { + (Account? account) { + if (account == null) { + return; + } balance.addAll(getHavenBalance(accountIndex: account.id)); walletAddresses.updateSubaddressList(accountIndex: account.id); }); @@ -74,15 +78,15 @@ abstract class HavenWalletBase extends WalletBase init() async { await walletAddresses.init(); - balance.addAll(getHavenBalance(accountIndex: walletAddresses.account.id ?? 0)); + balance.addAll(getHavenBalance(accountIndex: walletAddresses.account?.id ?? 0)); _setListeners(); await updateTransactions(); @@ -103,19 +107,19 @@ abstract class HavenWalletBase extends WalletBase connectToNode({@required Node node}) async { + Future connectToNode({required Node node}) async { try { syncStatus = ConnectingSyncStatus(); await haven_wallet.setupNode( address: node.uriRaw, login: node.login, password: node.password, - useSSL: node.useSSL, + useSSL: node.useSSL ?? false, isLightWallet: false); // FIXME: hardcoded value syncStatus = ConnectedSyncStatus(); } catch (e) { @@ -148,8 +152,8 @@ abstract class HavenWalletBase extends WalletBase 1; final assetType = CryptoCurrency.fromString(_credentials.assetType.toLowerCase()); - final balances = getHavenBalance(accountIndex: walletAddresses.account.id); - final unlockedBalance = balances[assetType].unlockedBalance; + final balances = getHavenBalance(accountIndex: walletAddresses.account!.id); + final unlockedBalance = balances[assetType]!.unlockedBalance; PendingTransactionDescription pendingTransactionDescription; @@ -159,12 +163,12 @@ abstract class HavenWalletBase extends WalletBase item.sendAll - || item.formattedCryptoAmount <= 0)) { + || (item.formattedCryptoAmount ?? 0) <= 0)) { throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); } final int totalAmount = outputs.fold(0, (acc, value) => - acc + value.formattedCryptoAmount); + acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); @@ -173,21 +177,21 @@ abstract class HavenWalletBase extends WalletBase MoneroOutput( address: output.address, - amount: output.cryptoAmount.replaceAll(',', '.'))) + amount: output.cryptoAmount!.replaceAll(',', '.'))) .toList(); pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account.id); + accountIndex: walletAddresses.account!.id); } else { final output = outputs.first; final address = output.address; final amount = output.sendAll ? null - : output.cryptoAmount.replaceAll(',', '.'); - final formattedAmount = output.sendAll + : output.cryptoAmount!.replaceAll(',', '.'); + final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; @@ -205,14 +209,14 @@ abstract class HavenWalletBase extends WalletBase rescan({int height}) async { + Future rescan({required int height}) async { walletInfo.restoreHeight = height; walletInfo.isRecovery = true; haven_wallet.setRefreshFromBlockHeight(height: height); @@ -346,7 +350,7 @@ abstract class HavenWalletBase extends WalletBase - balance.addAll(getHavenBalance(accountIndex: walletAddresses.account.id)); + balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); Future _askForUpdateTransactionHistory() async => await updateTransactions(); diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index 7afad6205..ff6b6aa3f 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -12,21 +12,22 @@ class HavenWalletAddresses = HavenWalletAddressesBase with _$HavenWalletAddresses; abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount with Store { - HavenWalletAddressesBase(WalletInfo walletInfo) : super(walletInfo) { - accountList = HavenAccountList(); - subaddressList = HavenSubaddressList(); - } + HavenWalletAddressesBase(WalletInfo walletInfo) + : accountList = HavenAccountList(), + subaddressList = HavenSubaddressList(), + address = '', + super(walletInfo); @override @observable String address; - @override + // @override @observable - Account account; + Account? account; @observable - Subaddress subaddress; + Subaddress? subaddress; HavenSubaddressList subaddressList; @@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount init() async { accountList.update(); account = accountList.accounts.first; - updateSubaddressList(accountIndex: account.id ?? 0); + updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -62,14 +63,14 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount info.id == WalletBase.idFor(name, getType()), - orElse: () => null); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(name, getType()))!; final wallet = HavenWallet(walletInfo: walletInfo); final isValid = wallet.walletAddresses.validate(); @@ -155,13 +158,13 @@ class HavenWalletService extends WalletService< final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromKeys( path: path, - password: credentials.password, + password: credentials.password!, language: credentials.language, - restoreHeight: credentials.height, + restoreHeight: credentials.height!, address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = HavenWallet(walletInfo: credentials.walletInfo); + final wallet = HavenWallet(walletInfo: credentials.walletInfo!); await wallet.init(); return wallet; @@ -179,10 +182,10 @@ class HavenWalletService extends WalletService< final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromSeed( path: path, - password: credentials.password, + password: credentials.password!, seed: credentials.mnemonic, - restoreHeight: credentials.height); - final wallet = HavenWallet(walletInfo: credentials.walletInfo); + restoreHeight: credentials.height!); + final wallet = HavenWallet(walletInfo: credentials.walletInfo!); await wallet.init(); return wallet; diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart index d56b5096c..b362cdf34 100644 --- a/cw_haven/lib/pending_haven_transaction.dart +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -2,7 +2,7 @@ import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/core/amount_converter.dart'; +// import 'package:cake_wallet/core/amount_converter.dart'; import 'package:cw_core/pending_transaction.dart'; class DoubleSpendException implements Exception { @@ -25,13 +25,17 @@ class PendingHavenTransaction with PendingTransaction { @override String get hex => ''; + // FIX-ME: AmountConverter @override - String get amountFormatted => AmountConverter.amountIntToString( - cryptoCurrency, pendingTransactionDescription.amount); + String get amountFormatted => ''; + // AmountConverter.amountIntToString( + // cryptoCurrency, pendingTransactionDescription.amount); + // FIX-ME: AmountConverter @override - String get feeFormatted => AmountConverter.amountIntToString( - cryptoCurrency, pendingTransactionDescription.fee); + String get feeFormatted => ''; + // AmountConverter.amountIntToString( + // cryptoCurrency, pendingTransactionDescription.fee); @override Future commit() async { diff --git a/cw_haven/lib/update_haven_rate.dart b/cw_haven/lib/update_haven_rate.dart index 967247af5..a7d5b2c77 100644 --- a/cw_haven/lib/update_haven_rate.dart +++ b/cw_haven/lib/update_haven_rate.dart @@ -1,7 +1,7 @@ //import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_haven/balance_list.dart'; +import 'package:cw_haven/api/balance_list.dart'; //Future updateHavenRate(FiatConversionStore fiatConversionStore) async { // final rate = getRate(); diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index f9b6c4890..6d741c268 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -7,35 +7,35 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "4.7.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.3.1" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.8.1" + version: "1.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -49,42 +49,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.10" + version: "3.1.0" build_resolvers: dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "7.2.4" built_collection: dependency: transitive description: @@ -105,63 +105,49 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.3.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.2" cw_core: dependency: "direct main" description: @@ -175,35 +161,28 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.12" - dartx: - dependency: transitive - description: - name: dartx - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" + version: "2.2.4" encrypt: dependency: transitive description: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "5.0.1" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: "direct main" description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.2.1" file: dependency: transitive description: @@ -229,12 +208,19 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "1.1.0+2" + version: "2.0.6+4" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" glob: dependency: transitive description: @@ -248,42 +234,42 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.1.0" hive: dependency: transitive description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.4+1" + version: "2.2.3" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "1.1.3" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.1" intl: dependency: "direct main" description: @@ -297,7 +283,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.3" js: dependency: transitive description: @@ -311,7 +297,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.6.0" logging: dependency: transitive description: @@ -325,14 +311,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.8.0" mime: dependency: transitive description: @@ -346,63 +339,77 @@ packages: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "1.2.1+4" + version: "2.1.0" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.7+3" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.28" + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+8" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+3" + version: "2.0.7" pedantic: dependency: transitive description: @@ -423,14 +430,14 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.6.2" pool: dependency: transitive description: @@ -458,21 +465,21 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" + version: "1.2.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -484,14 +491,21 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.2.3" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -519,35 +533,28 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - time: - dependency: transitive - description: - name: time - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.1" + version: "0.4.12" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" typed_data: dependency: transitive description: @@ -561,7 +568,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" watcher: dependency: transitive description: @@ -575,21 +582,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.2.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4+1" + version: "2.6.1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0+2" yaml: dependency: transitive description: @@ -598,5 +605,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=2.8.1" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml index a8d8417be..28f2c315e 100644 --- a/cw_haven/pubspec.yaml +++ b/cw_haven/pubspec.yaml @@ -6,17 +6,17 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.17.5 <3.0.0" flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - ffi: ^0.1.3 - path_provider: ^1.4.0 - http: ^0.12.0+2 - mobx: ^1.2.1+2 - flutter_mobx: ^1.1.0+2 + ffi: ^1.1.2 + http: ^0.13.4 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 intl: ^0.17.0 cw_core: path: ../cw_core @@ -24,10 +24,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.10.3 - build_resolvers: ^1.3.10 - mobx_codegen: ^1.1.0+1 - hive_generator: ^0.8.1 + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + build_resolvers: ^2.0.9 + hive_generator: ^1.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index b8f717d0f..451ba5033 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -50,16 +50,16 @@ List getAllAccount() { .toList(); } -void addAccountSync({String label}) { - final labelPointer = Utf8.toUtf8(label); +void addAccountSync({required String label}) { + final labelPointer = label.toNativeUtf8(); accountAddNewNative(labelPointer); - free(labelPointer); + calloc.free(labelPointer); } -void setLabelForAccountSync({int accountIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); +void setLabelForAccountSync({required int accountIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); accountSetLabelNative(accountIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void _addAccount(String label) => addAccountSync(label: label); @@ -71,12 +71,12 @@ void _setLabelForAccount(Map args) { setLabelForAccountSync(label: label, accountIndex: accountIndex); } -Future addAccount({String label}) async { +Future addAccount({required String label}) async { await compute(_addAccount, label); await store(); } -Future setLabelForAccount({int accountIndex, String label}) async { +Future setLabelForAccount({required int accountIndex, required String label}) async { await compute( _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); await store(); diff --git a/cw_monero/lib/api/convert_utf8_to_string.dart b/cw_monero/lib/api/convert_utf8_to_string.dart index 7fa5a68df..41a6b648a 100644 --- a/cw_monero/lib/api/convert_utf8_to_string.dart +++ b/cw_monero/lib/api/convert_utf8_to_string.dart @@ -1,8 +1,8 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; -String convertUTF8ToString({Pointer pointer}) { - final str = Utf8.fromUtf8(pointer); - free(pointer); +String convertUTF8ToString({required Pointer pointer}) { + final str = pointer.toDartString(); + calloc.free(pointer); return str; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/connection_to_node_exception.dart b/cw_monero/lib/api/exceptions/connection_to_node_exception.dart index 6ee272b89..483b0a174 100644 --- a/cw_monero/lib/api/exceptions/connection_to_node_exception.dart +++ b/cw_monero/lib/api/exceptions/connection_to_node_exception.dart @@ -1,5 +1,5 @@ class ConnectionToNodeException implements Exception { - ConnectionToNodeException({this.message}); + ConnectionToNodeException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/creation_transaction_exception.dart b/cw_monero/lib/api/exceptions/creation_transaction_exception.dart index bb477d673..7b55ec074 100644 --- a/cw_monero/lib/api/exceptions/creation_transaction_exception.dart +++ b/cw_monero/lib/api/exceptions/creation_transaction_exception.dart @@ -1,5 +1,5 @@ class CreationTransactionException implements Exception { - CreationTransactionException({this.message}); + CreationTransactionException({required this.message}); final String message; diff --git a/cw_monero/lib/api/exceptions/setup_wallet_exception.dart b/cw_monero/lib/api/exceptions/setup_wallet_exception.dart index ce43c0ec6..b6e0c1f18 100644 --- a/cw_monero/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_monero/lib/api/exceptions/setup_wallet_exception.dart @@ -1,5 +1,5 @@ class SetupWalletException implements Exception { - SetupWalletException({this.message}); + SetupWalletException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_creation_exception.dart b/cw_monero/lib/api/exceptions/wallet_creation_exception.dart index 6b00445ad..6052366b9 100644 --- a/cw_monero/lib/api/exceptions/wallet_creation_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_creation_exception.dart @@ -1,5 +1,5 @@ class WalletCreationException implements Exception { - WalletCreationException({this.message}); + WalletCreationException({required this.message}); final String message; diff --git a/cw_monero/lib/api/exceptions/wallet_opening_exception.dart b/cw_monero/lib/api/exceptions/wallet_opening_exception.dart index 8d84b0f7e..df7a850a4 100644 --- a/cw_monero/lib/api/exceptions/wallet_opening_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_opening_exception.dart @@ -1,5 +1,5 @@ class WalletOpeningException implements Exception { - WalletOpeningException({this.message}); + WalletOpeningException({required this.message}); final String message; diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index 5f08437d4..c6b6c6ef7 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -1,5 +1,5 @@ class WalletRestoreFromKeysException implements Exception { - WalletRestoreFromKeysException({this.message}); + WalletRestoreFromKeysException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart index fd89e4161..004cd7958 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -1,5 +1,5 @@ class WalletRestoreFromSeedException implements Exception { - WalletRestoreFromSeedException({this.message}); + WalletRestoreFromSeedException({required this.message}); final String message; } \ No newline at end of file diff --git a/cw_monero/lib/api/monero_output.dart b/cw_monero/lib/api/monero_output.dart index 831ee1f22..5b468cb48 100644 --- a/cw_monero/lib/api/monero_output.dart +++ b/cw_monero/lib/api/monero_output.dart @@ -1,7 +1,5 @@ -import 'package:flutter/foundation.dart'; - class MoneroOutput { - MoneroOutput({@required this.address, @required this.amount}); + MoneroOutput({required this.address, required this.amount}); final String address; final String amount; diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 16f983480..f9e88153e 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -35,7 +35,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer, Pointer, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); typedef start_refresh = Void Function(); @@ -82,7 +82,7 @@ typedef account_set_label = Void Function( typedef transactions_refresh = Void Function(); -typedef get_tx_key = Pointer Function(Pointer txId); +typedef get_tx_key = Pointer? Function(Pointer txId); typedef transactions_count = Int64 Function(); diff --git a/cw_monero/lib/api/structs/account_row.dart b/cw_monero/lib/api/structs/account_row.dart index c3fc22de1..aa492ee0f 100644 --- a/cw_monero/lib/api/structs/account_row.dart +++ b/cw_monero/lib/api/structs/account_row.dart @@ -3,9 +3,10 @@ import 'package:ffi/ffi.dart'; class AccountRow extends Struct { @Int64() - int id; - Pointer label; + external int id; + + external Pointer label; - String getLabel() => Utf8.fromUtf8(label); + String getLabel() => label.toDartString(); int getId() => id; } diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index edbd2d0ff..656ed333f 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -3,32 +3,32 @@ import 'package:ffi/ffi.dart'; class PendingTransactionRaw extends Struct { @Int64() - int amount; + external int amount; @Int64() - int fee; + external int fee; - Pointer hash; + external Pointer hash; - Pointer hex; + external Pointer hex; - Pointer txKey; + external Pointer txKey; - String getHash() => Utf8.fromUtf8(hash); + String getHash() => hash.toDartString(); - String getHex() => Utf8.fromUtf8(hex); + String getHex() => hex.toDartString(); - String getKey() => Utf8.fromUtf8(txKey); + String getKey() => txKey.toDartString(); } class PendingTransactionDescription { PendingTransactionDescription({ - this.amount, - this.fee, - this.hash, - this.hex, - this.txKey, - this.pointerAddress}); + required this.amount, + required this.fee, + required this.hash, + required this.hex, + required this.txKey, + required this.pointerAddress}); final int amount; final int fee; diff --git a/cw_monero/lib/api/structs/subaddress_row.dart b/cw_monero/lib/api/structs/subaddress_row.dart index 1673e00c7..d593a793d 100644 --- a/cw_monero/lib/api/structs/subaddress_row.dart +++ b/cw_monero/lib/api/structs/subaddress_row.dart @@ -3,11 +3,13 @@ import 'package:ffi/ffi.dart'; class SubaddressRow extends Struct { @Int64() - int id; - Pointer address; - Pointer label; + external int id; + + external Pointer address; + + external Pointer label; - String getLabel() => Utf8.fromUtf8(label); - String getAddress() => Utf8.fromUtf8(address); + String getLabel() => label.toDartString(); + String getAddress() => address.toDartString(); int getId() => id; } \ No newline at end of file diff --git a/cw_monero/lib/api/structs/transaction_info_row.dart b/cw_monero/lib/api/structs/transaction_info_row.dart index 37b0d02e8..bdcc64d3f 100644 --- a/cw_monero/lib/api/structs/transaction_info_row.dart +++ b/cw_monero/lib/api/structs/transaction_info_row.dart @@ -3,39 +3,39 @@ import 'package:ffi/ffi.dart'; class TransactionInfoRow extends Struct { @Uint64() - int amount; + external int amount; @Uint64() - int fee; + external int fee; @Uint64() - int blockHeight; + external int blockHeight; @Uint64() - int confirmations; + external int confirmations; @Uint32() - int subaddrAccount; + external int subaddrAccount; @Int8() - int direction; + external int direction; @Int8() - int isPending; + external int isPending; @Uint32() - int subaddrIndex; + external int subaddrIndex; - Pointer hash; + external Pointer hash; - Pointer paymentId; + external Pointer paymentId; @Int64() - int datetime; + external int datetime; int getDatetime() => datetime; int getAmount() => amount >= 0 ? amount : amount * -1; bool getIsPending() => isPending != 0; - String getHash() => Utf8.fromUtf8(hash); - String getPaymentId() => Utf8.fromUtf8(paymentId); + String getHash() => hash.toDartString(); + String getPaymentId() => paymentId.toDartString(); } diff --git a/cw_monero/lib/api/structs/ut8_box.dart b/cw_monero/lib/api/structs/ut8_box.dart index a6f41bc75..53e678c88 100644 --- a/cw_monero/lib/api/structs/ut8_box.dart +++ b/cw_monero/lib/api/structs/ut8_box.dart @@ -2,7 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; class Utf8Box extends Struct { - Pointer value; + external Pointer value; - String getValue() => Utf8.fromUtf8(value); + String getValue() => value.toDartString(); } diff --git a/cw_monero/lib/api/subaddress_list.dart b/cw_monero/lib/api/subaddress_list.dart index 88acb743a..1c1f1253f 100644 --- a/cw_monero/lib/api/subaddress_list.dart +++ b/cw_monero/lib/api/subaddress_list.dart @@ -29,7 +29,7 @@ final subaddrressSetLabelNative = moneroApi bool isUpdating = false; -void refreshSubaddresses({@required int accountIndex}) { +void refreshSubaddresses({required int accountIndex}) { try { isUpdating = true; subaddressRefreshNative(accountIndex); @@ -50,18 +50,18 @@ List getAllSubaddresses() { .toList(); } -void addSubaddressSync({int accountIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); +void addSubaddressSync({required int accountIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); subaddrressAddNewNative(accountIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void setLabelForSubaddressSync( - {int accountIndex, int addressIndex, String label}) { - final labelPointer = Utf8.toUtf8(label); + {required int accountIndex, required int addressIndex, required String label}) { + final labelPointer = label.toNativeUtf8(); subaddrressSetLabelNative(accountIndex, addressIndex, labelPointer); - free(labelPointer); + calloc.free(labelPointer); } void _addSubaddress(Map args) { @@ -80,14 +80,14 @@ void _setLabelForSubaddress(Map args) { accountIndex: accountIndex, addressIndex: addressIndex, label: label); } -Future addSubaddress({int accountIndex, String label}) async { +Future addSubaddress({required int accountIndex, required String label}) async { await compute, void>( _addSubaddress, {'accountIndex': accountIndex, 'label': label}); await store(); } -Future setLabelForSubaddress( - {int accountIndex, int addressIndex, String label}) async { +Future setLabelForSubaddress( + {required int accountIndex, required int addressIndex, required String label}) async { await compute, void>(_setLabelForSubaddress, { 'accountIndex': accountIndex, 'addressIndex': addressIndex, diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 9546a93d3..e9e161f42 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -40,16 +40,16 @@ final getTxKeyNative = moneroApi .asFunction(); String getTxKey(String txId) { - final txIdPointer = Utf8.toUtf8(txId); + final txIdPointer = txId.toNativeUtf8(); final keyPointer = getTxKeyNative(txIdPointer); - free(txIdPointer); + calloc.free(txIdPointer); if (keyPointer != null) { return convertUTF8ToString(pointer: keyPointer); } - return null; + return ''; } void refreshTransactions() => transactionsRefreshNative(); @@ -67,16 +67,16 @@ List getAllTransations() { } PendingTransactionDescription createTransactionSync( - {String address, - String paymentId, - String amount, - int priorityRaw, + {required String address, + required String paymentId, + required int priorityRaw, + String? amount, int accountIndex = 0}) { - final addressPointer = Utf8.toUtf8(address); - final paymentIdPointer = Utf8.toUtf8(paymentId); - final amountPointer = amount != null ? Utf8.toUtf8(amount) : nullptr; - final errorMessagePointer = allocate(); - final pendingTransactionRawPointer = allocate(); + final addressPointer = address.toNativeUtf8(); + final paymentIdPointer = paymentId.toNativeUtf8(); + final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + final errorMessagePointer = calloc(); + final pendingTransactionRawPointer = calloc(); final created = transactionCreateNative( addressPointer, paymentIdPointer, @@ -87,16 +87,16 @@ PendingTransactionDescription createTransactionSync( pendingTransactionRawPointer) != 0; - free(addressPointer); - free(paymentIdPointer); + calloc.free(addressPointer); + calloc.free(paymentIdPointer); if (amountPointer != nullptr) { - free(amountPointer); + calloc.free(amountPointer); } if (!created) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } @@ -110,26 +110,26 @@ PendingTransactionDescription createTransactionSync( } PendingTransactionDescription createTransactionMultDestSync( - {List outputs, - String paymentId, - int priorityRaw, + {required List outputs, + required String paymentId, + required int priorityRaw, int accountIndex = 0}) { final int size = outputs.length; final List> addressesPointers = outputs.map((output) => - Utf8.toUtf8(output.address)).toList(); - final Pointer> addressesPointerPointer = allocate(count: size); + output.address.toNativeUtf8()).toList(); + final Pointer> addressesPointerPointer = calloc(size); final List> amountsPointers = outputs.map((output) => - Utf8.toUtf8(output.amount)).toList(); - final Pointer> amountsPointerPointer = allocate(count: size); + output.amount.toNativeUtf8()).toList(); + final Pointer> amountsPointerPointer = calloc(size); for (int i = 0; i < size; i++) { addressesPointerPointer[i] = addressesPointers[i]; amountsPointerPointer[i] = amountsPointers[i]; } - final paymentIdPointer = Utf8.toUtf8(paymentId); - final errorMessagePointer = allocate(); - final pendingTransactionRawPointer = allocate(); + final paymentIdPointer = paymentId.toNativeUtf8(); + final errorMessagePointer = calloc(); + final pendingTransactionRawPointer = calloc(); final created = transactionCreateMultDestNative( addressesPointerPointer, paymentIdPointer, @@ -141,17 +141,17 @@ PendingTransactionDescription createTransactionMultDestSync( pendingTransactionRawPointer) != 0; - free(addressesPointerPointer); - free(amountsPointerPointer); + calloc.free(addressesPointerPointer); + calloc.free(amountsPointerPointer); - addressesPointers.forEach((element) => free(element)); - amountsPointers.forEach((element) => free(element)); + addressesPointers.forEach((element) => calloc.free(element)); + amountsPointers.forEach((element) => calloc.free(element)); - free(paymentIdPointer); + calloc.free(paymentIdPointer); if (!created) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } @@ -164,17 +164,17 @@ PendingTransactionDescription createTransactionMultDestSync( pointerAddress: pendingTransactionRawPointer.address); } -void commitTransactionFromPointerAddress({int address}) => commitTransaction( +void commitTransactionFromPointerAddress({required int address}) => commitTransaction( transactionPointer: Pointer.fromAddress(address)); -void commitTransaction({Pointer transactionPointer}) { - final errorMessagePointer = allocate(); +void commitTransaction({required Pointer transactionPointer}) { + final errorMessagePointer = calloc(); final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; if (!isCommited) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw CreationTransactionException(message: message); } } @@ -208,10 +208,10 @@ PendingTransactionDescription _createTransactionMultDestSync(Map args) { } Future createTransaction( - {String address, + {required String address, + required int priorityRaw, + String? amount, String paymentId = '', - String amount, - int priorityRaw, int accountIndex = 0}) => compute(_createTransactionSync, { 'address': address, @@ -222,9 +222,9 @@ Future createTransaction( }); Future createTransactionMultDest( - {List outputs, + {required List outputs, + required int priorityRaw, String paymentId = '', - int priorityRaw, int accountIndex = 0}) => compute(_createTransactionMultDestSync, { 'outputs': outputs, diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 3438b89fc..468ce93e2 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -35,7 +35,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer, Pointer, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer); typedef StartRefresh = void Function(); @@ -80,7 +80,7 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer label); typedef TransactionsRefresh = void Function(); -typedef GetTxKey = Pointer Function(Pointer txId); +typedef GetTxKey = Pointer? Function(Pointer txId); typedef TransactionsCount = int Function(); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 9e84d7865..5c2f1bd27 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -146,24 +146,24 @@ int getNodeHeightSync() => getNodeHeightNative(); bool isConnectedSync() => isConnectedNative() != 0; bool setupNodeSync( - {String address, - String login, - String password, + {required String address, + String? login, + String? password, bool useSSL = false, bool isLightWallet = false}) { - final addressPointer = Utf8.toUtf8(address); - Pointer loginPointer; - Pointer passwordPointer; + final addressPointer = address.toNativeUtf8(); + Pointer? loginPointer; + Pointer? passwordPointer; if (login != null) { - loginPointer = Utf8.toUtf8(login); + loginPointer = login.toNativeUtf8(); } if (password != null) { - passwordPointer = Utf8.toUtf8(password); + passwordPointer = password.toNativeUtf8(); } - final errorMessagePointer = allocate(); + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, loginPointer, @@ -173,9 +173,15 @@ bool setupNodeSync( errorMessagePointer) != 0; - free(addressPointer); - free(loginPointer); - free(passwordPointer); + calloc.free(addressPointer); + + if (loginPointer != null) { + calloc.free(loginPointer); + } + + if (passwordPointer != null) { + calloc.free(passwordPointer); + } if (!isSetupNode) { throw SetupWalletException( @@ -189,31 +195,31 @@ void startRefreshSync() => startRefreshNative(); Future connectToNode() async => connecToNodeNative() != 0; -void setRefreshFromBlockHeight({int height}) => +void setRefreshFromBlockHeight({required int height}) => setRefreshFromBlockHeightNative(height); -void setRecoveringFromSeed({bool isRecovery}) => +void setRecoveringFromSeed({required bool isRecovery}) => setRecoveringFromSeedNative(_boolToInt(isRecovery)); void storeSync() { - final pathPointer = Utf8.toUtf8(''); + final pathPointer = ''.toNativeUtf8(); storeNative(pathPointer); - free(pathPointer); + calloc.free(pathPointer); } void setPasswordSync(String password) { - final passwordPointer = Utf8.toUtf8(password); - final errorMessagePointer = allocate(); + final passwordPointer = password.toNativeUtf8(); + final errorMessagePointer = calloc(); final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; - free(passwordPointer); + calloc.free(passwordPointer); if (!changed) { final message = errorMessagePointer.ref.getValue(); - free(errorMessagePointer); + calloc.free(errorMessagePointer); throw Exception(message); } - free(errorMessagePointer); + calloc.free(errorMessagePointer); } void closeCurrentWallet() => closeCurrentWalletNative(); @@ -231,16 +237,16 @@ String getPublicSpendKey() => convertUTF8ToString(pointer: getPublicSpendKeyNative()); class SyncListener { - SyncListener(this.onNewBlock, this.onNewTransaction) { - _cachedBlockchainHeight = 0; - _lastKnownBlockHeight = 0; + SyncListener(this.onNewBlock, this.onNewTransaction) + : _cachedBlockchainHeight = 0, + _lastKnownBlockHeight = 0, _initialSyncHeight = 0; - } + void Function(int, int, double) onNewBlock; void Function() onNewTransaction; - Timer _updateSyncInfoTimer; + Timer? _updateSyncInfoTimer; int _cachedBlockchainHeight; int _lastKnownBlockHeight; int _initialSyncHeight; @@ -260,7 +266,7 @@ class SyncListener { _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async { if (isNewTransactionExist()) { - onNewTransaction?.call(); + onNewTransaction(); } var syncHeight = getSyncingHeight(); @@ -308,7 +314,7 @@ void onStartup() => onStartupNative(); void _storeSync(Object _) => storeSync(); -bool _setupNodeSync(Map args) { +bool _setupNodeSync(Map args) { final address = args['address'] as String; final login = (args['login'] ?? '') as String; final password = (args['password'] ?? '') as String; @@ -329,21 +335,21 @@ int _getNodeHeight(Object _) => getNodeHeightSync(); void startRefresh() => startRefreshSync(); -Future setupNode( - {String address, - String login, - String password, +Future setupNode( + {required String address, + String? login, + String? password, bool useSSL = false, bool isLightWallet = false}) => - compute, void>(_setupNodeSync, { + compute, void>(_setupNodeSync, { 'address': address, - 'login': login, + 'login': login , 'password': password, 'useSSL': useSSL, 'isLightWallet': isLightWallet }); -Future store() => compute(_storeSync, 0); +Future store() => compute(_storeSync, 0); Future isConnected() => compute(_isConnected, 0); diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index b414c8b0a..093d7e63b 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -38,18 +38,21 @@ final errorStringNative = moneroApi .asFunction(); void createWalletSync( - {String path, String password, String language, int nettype = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final languagePointer = Utf8.toUtf8(language); - final errorMessagePointer = allocate(); + {required String path, + required String password, + required String language, + int nettype = 0}) { + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final languagePointer = language.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletCreated = createWalletNative(pathPointer, passwordPointer, languagePointer, nettype, errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(languagePointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(languagePointer); if (!isWalletCreated) { throw WalletCreationException( @@ -59,25 +62,25 @@ void createWalletSync( // setupNodeSync(address: "node.moneroworld.com:18089"); } -bool isWalletExistSync({String path}) { - final pathPointer = Utf8.toUtf8(path); +bool isWalletExistSync({required String path}) { + final pathPointer = path.toNativeUtf8(); final isExist = isWalletExistNative(pathPointer) != 0; - free(pathPointer); + calloc.free(pathPointer); return isExist; } void restoreWalletFromSeedSync( - {String path, - String password, - String seed, + {required String path, + required String password, + required String seed, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final seedPointer = Utf8.toUtf8(seed); - final errorMessagePointer = allocate(); + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final seedPointer = seed.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletRestored = restoreWalletFromSeedNative( pathPointer, passwordPointer, @@ -87,9 +90,9 @@ void restoreWalletFromSeedSync( errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(seedPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(seedPointer); if (!isWalletRestored) { throw WalletRestoreFromSeedException( @@ -98,21 +101,21 @@ void restoreWalletFromSeedSync( } void restoreWalletFromKeysSync( - {String path, - String password, - String language, - String address, - String viewKey, - String spendKey, + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); - final languagePointer = Utf8.toUtf8(language); - final addressPointer = Utf8.toUtf8(address); - final viewKeyPointer = Utf8.toUtf8(viewKey); - final spendKeyPointer = Utf8.toUtf8(spendKey); - final errorMessagePointer = allocate(); + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); + final languagePointer = language.toNativeUtf8(); + final addressPointer = address.toNativeUtf8(); + final viewKeyPointer = viewKey.toNativeUtf8(); + final spendKeyPointer = spendKey.toNativeUtf8(); + final errorMessagePointer = ''.toNativeUtf8(); final isWalletRestored = restoreWalletFromKeysNative( pathPointer, passwordPointer, @@ -125,12 +128,12 @@ void restoreWalletFromKeysSync( errorMessagePointer) != 0; - free(pathPointer); - free(passwordPointer); - free(languagePointer); - free(addressPointer); - free(viewKeyPointer); - free(spendKeyPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); + calloc.free(languagePointer); + calloc.free(addressPointer); + calloc.free(viewKeyPointer); + calloc.free(spendKeyPointer); if (!isWalletRestored) { throw WalletRestoreFromKeysException( @@ -138,12 +141,15 @@ void restoreWalletFromKeysSync( } } -void loadWallet({String path, String password, int nettype = 0}) { - final pathPointer = Utf8.toUtf8(path); - final passwordPointer = Utf8.toUtf8(password); +void loadWallet({ + required String path, + required String password, + int nettype = 0}) { + final pathPointer = path.toNativeUtf8(); + final passwordPointer = password.toNativeUtf8(); final loaded = loadWalletNative(pathPointer, passwordPointer, nettype) != 0; - free(pathPointer); - free(passwordPointer); + calloc.free(pathPointer); + calloc.free(passwordPointer); if (!loaded) { throw WalletOpeningException( @@ -189,20 +195,20 @@ void _restoreFromKeys(Map args) { } Future _openWallet(Map args) async => - loadWallet(path: args['path'], password: args['password']); + loadWallet(path: args['path'] as String, password: args['password'] as String); bool _isWalletExist(String path) => isWalletExistSync(path: path); -void openWallet({String path, String password, int nettype = 0}) async => +void openWallet({required String path, required String password, int nettype = 0}) async => loadWallet(path: path, password: password, nettype: nettype); Future openWalletAsync(Map args) async => compute(_openWallet, args); Future createWallet( - {String path, - String password, - String language, + {required String path, + required String password, + required String language, int nettype = 0}) async => compute(_createWallet, { 'path': path, @@ -211,10 +217,10 @@ Future createWallet( 'nettype': nettype }); -Future restoreFromSeed( - {String path, - String password, - String seed, +Future restoreFromSeed( + {required String path, + required String password, + required String seed, int nettype = 0, int restoreHeight = 0}) async => compute, void>(_restoreFromSeed, { @@ -225,13 +231,13 @@ Future restoreFromSeed( 'restoreHeight': restoreHeight }); -Future restoreFromKeys( - {String path, - String password, - String language, - String address, - String viewKey, - String spendKey, +Future restoreFromKeys( + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, int nettype = 0, int restoreHeight = 0}) async => compute, void>(_restoreFromKeys, { @@ -245,4 +251,4 @@ Future restoreFromKeys( 'restoreHeight': restoreHeight }); -Future isWalletExist({String path}) => compute(_isWalletExist, path); +Future isWalletExist({required String path}) => compute(_isWalletExist, path); diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index 12cda5680..f618cf57a 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -12,7 +12,6 @@ abstract class MoneroAccountListBase with Store { _isRefreshing = false, _isUpdating = false { refresh(); - print(account_list.accountSizeNative()); } @observable @@ -49,12 +48,12 @@ abstract class MoneroAccountListBase with Store { label: accountRow.getLabel())) .toList(); - Future addAccount({String label}) async { + Future addAccount({required String label}) async { await account_list.addAccount(label: label); update(); } - Future setLabelAccount({int accountIndex, String label}) async { + Future setLabelAccount({required int accountIndex, required String label}) async { await account_list.setLabelForAccount( accountIndex: accountIndex, label: label); update(); diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index e59052207..8d8eeb469 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -10,11 +10,10 @@ class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList; abstract class MoneroSubaddressListBase with Store { - MoneroSubaddressListBase() { - _isRefreshing = false; - _isUpdating = false; - subaddresses = ObservableList(); - } + MoneroSubaddressListBase() + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList(); @observable ObservableList subaddresses; @@ -22,7 +21,7 @@ abstract class MoneroSubaddressListBase with Store { bool _isRefreshing; bool _isUpdating; - void update({int accountIndex}) { + void update({required int accountIndex}) { if (_isUpdating) { return; } @@ -59,20 +58,20 @@ abstract class MoneroSubaddressListBase with Store { .toList(); } - Future addSubaddress({int accountIndex, String label}) async { + Future addSubaddress({required int accountIndex, required String label}) async { await subaddress_list.addSubaddress( accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } - Future setLabelSubaddress( - {int accountIndex, int addressIndex, String label}) async { + Future setLabelSubaddress( + {required int accountIndex, required int addressIndex, required String label}) async { await subaddress_list.setLabelForSubaddress( accountIndex: accountIndex, addressIndex: addressIndex, label: label); update(accountIndex: accountIndex); } - void refresh({int accountIndex}) { + void refresh({required int accountIndex}) { if (_isRefreshing) { return; } diff --git a/cw_monero/lib/monero_transaction_creation_credentials.dart b/cw_monero/lib/monero_transaction_creation_credentials.dart index 3f0051046..96f2b1637 100644 --- a/cw_monero/lib/monero_transaction_creation_credentials.dart +++ b/cw_monero/lib/monero_transaction_creation_credentials.dart @@ -2,7 +2,7 @@ import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class MoneroTransactionCreationCredentials { - MoneroTransactionCreationCredentials({this.outputs, this.priority}); + MoneroTransactionCreationCredentials({required this.outputs, required this.priority}); final List outputs; final MoneroTransactionPriority priority; diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index db393497a..2dfdaf408 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -10,7 +10,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo(this.id, this.height, this.direction, this.date, this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee); - MoneroTransactionInfo.fromMap(Map map) + MoneroTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, direction = @@ -24,7 +24,7 @@ class MoneroTransactionInfo extends TransactionInfo { addressIndex = map['addressIndex'] as int, key = getTxKey((map['hash'] ?? '') as String), fee = map['fee'] as int ?? 0 { - additionalInfo = { + additionalInfo = { 'key': key, 'accountIndex': accountIndex, 'addressIndex': addressIndex @@ -43,7 +43,7 @@ class MoneroTransactionInfo extends TransactionInfo { addressIndex = row.subaddrIndex, key = getTxKey(row.getHash()), fee = row.fee { - additionalInfo = { + additionalInfo = { 'key': key, 'accountIndex': accountIndex, 'addressIndex': addressIndex @@ -59,10 +59,9 @@ class MoneroTransactionInfo extends TransactionInfo { final int amount; final int fee; final int addressIndex; - String recipientAddress; - String key; - - String _fiatAmount; + String? recipientAddress; + String? key; + String? _fiatAmount; @override String amountFormatted() => diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 175bd96f5..956c9b9fc 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -36,19 +36,24 @@ class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({WalletInfo walletInfo}) - : super(walletInfo) { + MoneroWalletBase({required WalletInfo walletInfo}) + : balance = ObservableMap.of({ + CryptoCurrency.xmr: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: 0), + unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) + }), + _isTransactionUpdating = false, + _hasSyncAfterStartup = false, + walletAddresses = MoneroWalletAddresses(walletInfo), + syncStatus = NotConnectedSyncStatus(), + super(walletInfo) { transactionHistory = MoneroTransactionHistory(); - balance = ObservableMap.of({ - CryptoCurrency.xmr: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: 0), - unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) - }); - _isTransactionUpdating = false; - _hasSyncAfterStartup = false; - walletAddresses = MoneroWalletAddresses(walletInfo); _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account account) { + (Account? account) { + if (account == null) { + return; + } + balance = ObservableMap.of( { currency: MoneroBalance( @@ -83,19 +88,19 @@ abstract class MoneroWalletBase extends WalletBase init() async { await walletAddresses.init(); balance = ObservableMap.of( { currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account.id), - unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id)) + fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) }); _setListeners(); await updateTransactions(); @@ -117,12 +122,12 @@ abstract class MoneroWalletBase extends WalletBase connectToNode({@required Node node}) async { + Future connectToNode({required Node node}) async { try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNode( @@ -162,7 +167,7 @@ abstract class MoneroWalletBase extends WalletBase 1; final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id); + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); PendingTransactionDescription pendingTransactionDescription; @@ -172,32 +177,32 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll - || item.formattedCryptoAmount <= 0)) { + || (item.formattedCryptoAmount ?? 0) <= 0)) { throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); } final int totalAmount = outputs.fold(0, (acc, value) => - acc + value.formattedCryptoAmount); + acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); } final moneroOutputs = outputs.map((output) { - final outputAddress = output.isParsedAddress - ? output.extractedAddress - : output.address; + final outputAddress = output.isParsedAddress + ? output.extractedAddress + : output.address; - return MoneroOutput( - address: outputAddress, - amount: output.cryptoAmount.replaceAll(',', '.')); + return MoneroOutput( + address: outputAddress!, + amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account.id); + accountIndex: walletAddresses.account!.id); } else { final output = outputs.first; final address = output.isParsedAddress @@ -205,7 +210,7 @@ abstract class MoneroWalletBase extends WalletBase rescan({int height}) async { + Future rescan({required int height}) async { walletInfo.restoreHeight = height; walletInfo.isRecovery = true; monero_wallet.setRefreshFromBlockHeight(height: height); @@ -372,8 +377,8 @@ abstract class MoneroWalletBase extends WalletBase - monero_wallet.getFullBalance(accountIndex: walletAddresses.account.id); + monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); int _getUnlockedBalance() => - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id); + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); void _onNewBlock(int height, int blocksLeft, double ptc) async { try { diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index c84e8c8e4..2002e789a 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -12,20 +12,21 @@ class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses; abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { - MoneroWalletAddressesBase(WalletInfo walletInfo) : super(walletInfo) { - accountList = MoneroAccountList(); - subaddressList = MoneroSubaddressList(); - } + MoneroWalletAddressesBase(WalletInfo walletInfo) + : accountList = MoneroAccountList(), + subaddressList = MoneroSubaddressList(), + address = '', + super(walletInfo); @override @observable String address; + + @observable + Account? account; @observable - Account account; - - @observable - Subaddress subaddress; + Subaddress? subaddress; MoneroSubaddressList subaddressList; @@ -35,7 +36,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { Future init() async { accountList.update(); account = accountList.accounts.first; - updateSubaddressList(accountIndex: account.id ?? 0); + updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -61,14 +62,14 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { bool validate() { accountList.update(); - final accountListLength = accountList.accounts?.length ?? 0; + final accountListLength = accountList.accounts.length ?? 0; if (accountListLength <= 0) { return false; } subaddressList.update(accountIndex: accountList.accounts.first.id); - final subaddressListLength = subaddressList.subaddresses?.length ?? 0; + final subaddressListLength = subaddressList.subaddresses.length ?? 0; if (subaddressListLength <= 0) { return false; @@ -77,9 +78,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { return true; } - void updateSubaddressList({int accountIndex}) { + void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); subaddress = subaddressList.subaddresses.first; - address = subaddress.address; + address = subaddress!.address; } } \ No newline at end of file diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index d0461de72..095fe83bb 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -13,7 +13,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; class MoneroNewWalletCredentials extends WalletCredentials { - MoneroNewWalletCredentials({String name, String password, this.language}) + MoneroNewWalletCredentials({required String name, required this.language, String? password}) : super(name: name, password: password); final String language; @@ -21,7 +21,7 @@ class MoneroNewWalletCredentials extends WalletCredentials { class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { MoneroRestoreWalletFromSeedCredentials( - {String name, String password, int height, this.mnemonic}) + {required String name, required this.mnemonic, int height = 0, String? password}) : super(name: name, password: password, height: height); final String mnemonic; @@ -34,13 +34,13 @@ class MoneroWalletLoadingException implements Exception { class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { MoneroRestoreWalletFromKeysCredentials( - {String name, - String password, - this.language, - this.address, - this.viewKey, - this.spendKey, - int height}) + {required String name, + required String password, + required this.language, + required this.address, + required this.viewKey, + required this.spendKey, + int height = 0}) : super(name: name, password: password, height: height); final String language; @@ -69,9 +69,9 @@ class MoneroWalletService extends WalletService< final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.createWallet( path: path, - password: credentials.password, + password: credentials.password!, language: credentials.language); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); await wallet.init(); return wallet; @@ -106,8 +106,7 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType()), - orElse: () => null); + (info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet(walletInfo: walletInfo); final isValid = wallet.walletAddresses.validate(); @@ -156,13 +155,13 @@ class MoneroWalletService extends WalletService< final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromKeys( path: path, - password: credentials.password, + password: credentials.password!, language: credentials.language, - restoreHeight: credentials.height, + restoreHeight: credentials.height!, address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); await wallet.init(); return wallet; @@ -180,10 +179,10 @@ class MoneroWalletService extends WalletService< final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromSeed( path: path, - password: credentials.password, + password: credentials.password!, seed: credentials.mnemonic, - restoreHeight: credentials.height); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo); + restoreHeight: credentials.height!); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); await wallet.init(); return wallet; diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index d32bab2ce..7488cfc08 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -2,7 +2,7 @@ import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/core/amount_converter.dart'; +// import 'package:cake_wallet/core/amount_converter.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -27,13 +27,17 @@ class PendingMoneroTransaction with PendingTransaction { String get txKey => pendingTransactionDescription.txKey; + // FIX-ME: AmountConverter @override - String get amountFormatted => AmountConverter.amountIntToString( - CryptoCurrency.xmr, pendingTransactionDescription.amount); + String get amountFormatted => ''; + // AmountConverter.amountIntToString( + // CryptoCurrency.xmr, pendingTransactionDescription.amount); + // FIX-ME: AmountConverter @override - String get feeFormatted => AmountConverter.amountIntToString( - CryptoCurrency.xmr, pendingTransactionDescription.fee); + String get feeFormatted => ''; + // AmountConverter.amountIntToString( + // CryptoCurrency.xmr, pendingTransactionDescription.fee); @override Future commit() async { diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 82c5f78f1..557550754 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -7,35 +7,35 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "4.7.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.3.1" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.8.1" + version: "1.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -49,42 +49,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.10" + version: "3.1.0" build_resolvers: dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "7.2.4" built_collection: dependency: transitive description: @@ -105,63 +105,49 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.3.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.2" cw_core: dependency: "direct main" description: @@ -175,35 +161,28 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.12" - dartx: - dependency: transitive - description: - name: dartx - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" + version: "2.2.4" encrypt: - dependency: transitive + dependency: "direct main" description: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "5.0.1" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: "direct main" description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.2.1" file: dependency: transitive description: @@ -229,12 +208,19 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "1.1.0+2" + version: "2.0.6+4" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" glob: dependency: transitive description: @@ -248,42 +234,42 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.1.0" hive: dependency: transitive description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.4+1" + version: "2.2.3" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "1.1.3" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.1" intl: dependency: "direct main" description: @@ -297,7 +283,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.3" js: dependency: transitive description: @@ -311,7 +297,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.6.0" logging: dependency: transitive description: @@ -325,14 +311,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.8.0" mime: dependency: transitive description: @@ -346,35 +339,77 @@ packages: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "1.2.1+4" + version: "2.1.0" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.7+3" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" pedantic: dependency: transitive description: @@ -388,14 +423,21 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.6.2" pool: dependency: transitive description: @@ -403,6 +445,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" pub_semver: dependency: transitive description: @@ -416,21 +465,21 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" + version: "1.2.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -442,14 +491,21 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.2.3" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -477,35 +533,28 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - time: - dependency: transitive - description: - name: time - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.1" + version: "0.4.12" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" typed_data: dependency: transitive description: @@ -519,7 +568,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" watcher: dependency: transitive description: @@ -533,7 +582,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" yaml: dependency: transitive description: @@ -542,5 +605,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=2.8.1" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 29584e194..23e8782cb 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -6,27 +6,29 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.17.5 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - ffi: ^0.1.3 - path_provider: ^1.4.0 - http: ^0.12.0+2 - mobx: ^1.2.1+2 - flutter_mobx: ^1.1.0+2 + ffi: ^1.1.2 + http: ^0.13.4 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 intl: ^0.17.0 + encrypt: ^5.0.1 cw_core: path: ../cw_core dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.10.3 - build_resolvers: ^1.3.10 - mobx_codegen: ^1.1.0+1 - hive_generator: ^0.8.1 + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 66762582e..ec8e4f2e3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - barcode_scan (0.0.1): + - barcode_scan2 (0.0.1): - Flutter - MTBBarcodeScanner - SwiftProtobuf @@ -98,22 +98,20 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif - - esys_flutter_share (0.0.1): - - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - flutter_secure_storage (3.3.1): - Flutter - - local_auth (0.0.1): + - local_auth_ios (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) - package_info (0.0.1): - Flutter - - path_provider (0.0.1): + - path_provider_ios (0.0.1): - Flutter - - "permission_handler (5.1.0+2)": + - permission_handler_apple (9.0.4): - Flutter - platform_device_id (0.0.1): - Flutter @@ -123,22 +121,22 @@ PODS: - SDWebImage/Core (5.9.1) - share (0.0.1): - Flutter - - shared_preferences (0.0.1): + - shared_preferences_ios (0.0.1): - Flutter - - SwiftProtobuf (1.12.0) + - SwiftProtobuf (1.18.0) - SwiftyGif (5.3.0) - uni_links (0.0.1): - Flutter - UnstoppableDomainsResolution (4.0.0): - BigInt - CryptoSwift - - url_launcher (0.0.1): + - url_launcher_ios (0.0.1): - Flutter - - webview_flutter (0.0.1): + - webview_flutter_wkwebview (0.0.1): - Flutter DEPENDENCIES: - - barcode_scan (from `.symlinks/plugins/barcode_scan/ios`) + - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity (from `.symlinks/plugins/connectivity/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) @@ -147,21 +145,20 @@ DEPENDENCIES: - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info (from `.symlinks/plugins/device_info/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - local_auth (from `.symlinks/plugins/local_auth/ios`) + - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - - path_provider (from `.symlinks/plugins/path_provider/ios`) - - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) - share (from `.symlinks/plugins/share/ios`) - - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -177,8 +174,8 @@ SPEC REPOS: - UnstoppableDomainsResolution EXTERNAL SOURCES: - barcode_scan: - :path: ".symlinks/plugins/barcode_scan/ios" + barcode_scan2: + :path: ".symlinks/plugins/barcode_scan2/ios" connectivity: :path: ".symlinks/plugins/connectivity/ios" cw_haven: @@ -193,37 +190,35 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/device_info/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" - esys_flutter_share: - :path: ".symlinks/plugins/esys_flutter_share/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" - local_auth: - :path: ".symlinks/plugins/local_auth/ios" + local_auth_ios: + :path: ".symlinks/plugins/local_auth_ios/ios" package_info: :path: ".symlinks/plugins/package_info/ios" - path_provider: - :path: ".symlinks/plugins/path_provider/ios" - permission_handler: - :path: ".symlinks/plugins/permission_handler/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" platform_device_id: :path: ".symlinks/plugins/platform_device_id/ios" share: :path: ".symlinks/plugins/share/ios" - shared_preferences: - :path: ".symlinks/plugins/shared_preferences/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" - url_launcher: - :path: ".symlinks/plugins/url_launcher/ios" - webview_flutter: - :path: ".symlinks/plugins/webview_flutter/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479 + barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 @@ -235,26 +230,25 @@ SPEC CHECKSUMS: devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 - file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 - Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c + file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec - local_auth: 25938960984c3a7f6e3253e3f8d962fdd16852bd + local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 - shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - SwiftProtobuf: 4ef85479c18ca85b5482b343df9c319c62bda699 + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + SwiftProtobuf: c3c12645230d9b09c72267e0de89468c5543bd86 SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef - webview_flutter: 3603125dfd3bcbc9d8d418c3f80aeecf331c068b + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f PODFILE CHECKSUM: ae71bdf0eb731a1ffc399c122f6aa4dea0cb5f6f diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a0808e686..8d62665c3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -162,7 +162,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -368,6 +368,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; + DISABLED_ARCHS = x86_64; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -390,6 +391,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -512,6 +514,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; + DISABLED_ARCHS = x86_64; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -535,6 +538,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -548,6 +552,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; + DISABLED_ARCHS = x86_64; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -570,6 +575,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; + VALID_ARCHS = arm64; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fb2dffc49..c87d15a33 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ obj) { final instructions = (obj['instructions'] as List) @@ -45,13 +45,13 @@ class AnyPayPayment { .fold(0, (int outAcc, out) => outAcc + out.amount)); switch (chain) { case AnyPayChain.xmr: - return monero.formatterMoneroAmountToString(amount: total); + return monero!.formatterMoneroAmountToString(amount: total); case AnyPayChain.btc: - return bitcoin.formatterBitcoinAmountToString(amount: total); + return bitcoin!.formatterBitcoinAmountToString(amount: total); case AnyPayChain.ltc: - return bitcoin.formatterBitcoinAmountToString(amount: total); + return bitcoin!.formatterBitcoinAmountToString(amount: total); default: - return null; + return ''; } } diff --git a/lib/anypay/any_pay_payment_committed_info.dart b/lib/anypay/any_pay_payment_committed_info.dart index 126b3d92e..12adea003 100644 --- a/lib/anypay/any_pay_payment_committed_info.dart +++ b/lib/anypay/any_pay_payment_committed_info.dart @@ -3,11 +3,11 @@ import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; class AnyPayPaymentCommittedInfo { const AnyPayPaymentCommittedInfo({ - @required this.uri, - @required this.currency, - @required this.chain, - @required this.transactions, - @required this.memo}); + required this.uri, + required this.currency, + required this.chain, + required this.transactions, + required this.memo}); final String uri; final String currency; diff --git a/lib/anypay/any_pay_payment_instruction.dart b/lib/anypay/any_pay_payment_instruction.dart index 41d2ee82d..6477e6649 100644 --- a/lib/anypay/any_pay_payment_instruction.dart +++ b/lib/anypay/any_pay_payment_instruction.dart @@ -3,11 +3,11 @@ import 'package:cake_wallet/anypay/any_pay_payment_instruction_output.dart'; class AnyPayPaymentInstruction { AnyPayPaymentInstruction({ - @required this.type, - @required this.requiredFeeRate, - @required this.txKey, - @required this.txHash, - @required this.outputs}); + required this.type, + required this.requiredFeeRate, + required this.txKey, + required this.txHash, + required this.outputs}); factory AnyPayPaymentInstruction.fromMap(Map obj) { final outputs = (obj['outputs'] as List) diff --git a/lib/anypay/any_pay_trasnaction.dart b/lib/anypay/any_pay_trasnaction.dart index 29f8a0152..af736cbf6 100644 --- a/lib/anypay/any_pay_trasnaction.dart +++ b/lib/anypay/any_pay_trasnaction.dart @@ -1,9 +1,7 @@ -import 'package:flutter/foundation.dart'; - class AnyPayTransaction { - const AnyPayTransaction(this.tx, {@required this.id, @required this.key}); + const AnyPayTransaction(this.tx, {required this.id, required this.key}); final String tx; final String id; - final String key; + final String? key; } \ No newline at end of file diff --git a/lib/anypay/anypay_api.dart b/lib/anypay/anypay_api.dart index c0727bc29..b53a3ebd1 100644 --- a/lib/anypay/anypay_api.dart +++ b/lib/anypay/anypay_api.dart @@ -33,15 +33,15 @@ class AnyPayApi { case 'litecoin': return CryptoCurrency.ltc; default: - return null; + throw Exception('Unexpected scheme: ${scheme}'); } } Future paymentRequest(String uri) async { final fragments = uri.split(':?r='); final scheme = fragments.first; - final url = fragments[1]; - final headers = { + final url = Uri.parse(fragments[1]); + final headers = { 'Content-Type': contentTypePaymentRequest, 'X-Paypro-Version': xPayproVersion, 'Accept': '*/*',}; @@ -50,20 +50,20 @@ class AnyPayApi { 'currency': currencyByScheme(scheme).title}; final response = await post(url, headers: headers, body: utf8.encode(json.encode(body))); - if (response.statusCode != 200) { - return null; + if (response.statusCode != 200) { + throw Exception('Unexpected response http code: ${response.statusCode}'); } - final decodedBody = json.decode(response.body) as Map; - return AnyPayPayment.fromMap(decodedBody); + final decodedBody = json.decode(response.body) as Map; + return AnyPayPayment.fromMap(decodedBody); } Future payment( String uri, - {@required String chain, - @required String currency, - @required List transactions}) async { - final headers = { + {required String chain, + required String currency, + required List transactions}) async { + final headers = { 'Content-Type': contentTypePayment, 'X-Paypro-Version': xPayproVersion, 'Accept': '*/*',}; @@ -71,7 +71,7 @@ class AnyPayApi { 'chain': chain, 'currency': currency, 'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()}; - final response = await post(uri, headers: headers, body: utf8.encode(json.encode(body))); + final response = await post(Uri.parse(uri), headers: headers, body: utf8.encode(json.encode(body))); if (response.statusCode == 400) { final decodedBody = json.decode(response.body) as Map; throw Exception(decodedBody['message'] as String); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 46ef89172..9d5484b79 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -5,15 +5,24 @@ class CWBitcoin extends Bitcoin { TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; @override - WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({String name, String mnemonic, String password}) + WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password}) => BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); @override - WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({String name, String password, String wif, WalletInfo walletInfo}) + WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({ + required String name, + required String password, + required String wif, + WalletInfo? walletInfo}) => BitcoinRestoreWalletFromWIFCredentials(name: name, password: password, wif: wif, walletInfo: walletInfo); @override - WalletCredentials createBitcoinNewWalletCredentials({String name, WalletInfo walletInfo}) + WalletCredentials createBitcoinNewWalletCredentials({ + required String name, + WalletInfo? walletInfo}) => BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); @override @@ -55,7 +64,7 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, {TransactionPriority priority, int feeRate}) + Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}) => BitcoinTransactionCredentials( outputs.map((out) => OutputInfo( fiatAmount: out.fiatAmount, @@ -71,7 +80,7 @@ class CWBitcoin extends Bitcoin { feeRate: feeRate); @override - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority priority, int feeRate}) + Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}) => BitcoinTransactionCredentials( outputs, priority: priority != null ? priority as BitcoinTransactionPriority : null, @@ -92,11 +101,11 @@ class CWBitcoin extends Bitcoin { } @override - String formatterBitcoinAmountToString({int amount}) + String formatterBitcoinAmountToString({required int amount}) => bitcoinAmountToString(amount: amount); @override - double formatterBitcoinAmountToDouble({int amount}) + double formatterBitcoinAmountToDouble({required int amount}) => bitcoinAmountToDouble(amount: amount); @override diff --git a/lib/buy/buy_amount.dart b/lib/buy/buy_amount.dart index 1a1b3ae28..e41bb1148 100644 --- a/lib/buy/buy_amount.dart +++ b/lib/buy/buy_amount.dart @@ -2,13 +2,13 @@ import 'package:flutter/foundation.dart'; class BuyAmount { BuyAmount({ - @required this.sourceAmount, - @required this.destAmount, + required this.sourceAmount, + required this.destAmount, this.achSourceAmount, this.minAmount = 0}); final double sourceAmount; final double destAmount; - final double achSourceAmount; + final double? achSourceAmount; final int minAmount; } \ No newline at end of file diff --git a/lib/buy/buy_exception.dart b/lib/buy/buy_exception.dart index 28064fdfc..edc6a7db0 100644 --- a/lib/buy/buy_exception.dart +++ b/lib/buy/buy_exception.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; class BuyException implements Exception { - BuyException({@required this.description, @required this.text}); + BuyException({required this.description, required this.text}); final BuyProviderDescription description; final String text; diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 0f496bad2..10a13ed94 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -5,7 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; abstract class BuyProvider { - BuyProvider({this.wallet, this.isTestEnvironment}); + BuyProvider({required this.wallet, required this.isTestEnvironment}); final WalletBase wallet; final bool isTestEnvironment; diff --git a/lib/buy/buy_provider_description.dart b/lib/buy/buy_provider_description.dart index bcb581d05..07c7ff08b 100644 --- a/lib/buy/buy_provider_description.dart +++ b/lib/buy/buy_provider_description.dart @@ -2,20 +2,20 @@ import 'package:cw_core/enumerable_item.dart'; class BuyProviderDescription extends EnumerableItem with Serializable { - const BuyProviderDescription({String title, int raw}) + const BuyProviderDescription({required String title, required int raw}) : super(title: title, raw: raw); static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0); static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1); - static BuyProviderDescription deserialize({int raw}) { + static BuyProviderDescription deserialize({required int raw}) { switch (raw) { case 0: return wyre; case 1: return moonPay; default: - return null; + throw Exception('Incorrect token $raw for BuyProviderDescription deserialize'); } } } \ No newline at end of file diff --git a/lib/buy/get_buy_provider_icon.dart b/lib/buy/get_buy_provider_icon.dart index 672dfdd9d..c755d9615 100644 --- a/lib/buy/get_buy_provider_icon.dart +++ b/lib/buy/get_buy_provider_icon.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; -Image getBuyProviderIcon(BuyProviderDescription providerDescription, +Image? getBuyProviderIcon(BuyProviderDescription providerDescription, {Color iconColor = Colors.black}) { final _wyreIcon = diff --git a/lib/buy/moonpay/moonpay_buy_provider.dart b/lib/buy/moonpay/moonpay_buy_provider.dart index 4248df483..4ff3fb04c 100644 --- a/lib/buy/moonpay/moonpay_buy_provider.dart +++ b/lib/buy/moonpay/moonpay_buy_provider.dart @@ -23,7 +23,7 @@ class MoonPaySellProvider { final bool isTest; final String baseUrl; - Future requestUrl({CryptoCurrency currency, String refundWalletAddress}) async { + Future requestUrl({required CryptoCurrency currency, required String refundWalletAddress}) async { final originalUri = Uri.https( baseUrl, '', { 'apiKey': _apiKey, @@ -48,10 +48,9 @@ class MoonPaySellProvider { } class MoonPayBuyProvider extends BuyProvider { - MoonPayBuyProvider({WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, isTestEnvironment: isTestEnvironment) { - baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl; - } + MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl, + super(wallet: wallet, isTestEnvironment: isTestEnvironment); static const _baseTestUrl = 'https://buy-staging.moonpay.com'; static const _baseProductUrl = 'https://buy.moonpay.com'; @@ -109,8 +108,8 @@ class MoonPayBuyProvider extends BuyProvider { _quoteSuffix + '/?apiKey=' + _apiKey + '&baseCurrencyAmount=' + amount + '&baseCurrencyCode=' + sourceCurrency.toLowerCase(); - - final response = await get(url); + final uri = Uri.parse(url); + final response = await get(uri); if (response.statusCode != 200) { throw BuyException( @@ -133,8 +132,8 @@ class MoonPayBuyProvider extends BuyProvider { Future findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; - - final response = await get(url); + final uri = Uri.parse(url); + final response = await get(uri); if (response.statusCode != 200) { throw BuyException( @@ -164,8 +163,8 @@ class MoonPayBuyProvider extends BuyProvider { static Future onEnabled() async { final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; var isBuyEnable = false; - - final response = await get(url); + final uri = Uri.parse(url); + final response = await get(uri); try { final responseJSON = json.decode(response.body) as Map; diff --git a/lib/buy/order.dart b/lib/buy/order.dart index 24c5f58db..16dfbb4e0 100644 --- a/lib/buy/order.dart +++ b/lib/buy/order.dart @@ -8,18 +8,23 @@ part 'order.g.dart'; @HiveType(typeId: Order.typeId) class Order extends HiveObject { Order( - {this.id, - BuyProviderDescription provider, - this.transferId, + {required this.id, + required this.transferId, + required this.createdAt, + required this.amount, + required this.receiveAddress, + required this.walletId, + BuyProviderDescription? provider, + TradeState? state, this.from, - this.to, - TradeState state, - this.createdAt, - this.amount, - this.receiveAddress, - this.walletId}) - : providerRaw = provider?.raw, - stateRaw = state?.raw; + this.to}) { + if (provider != null) { + providerRaw = provider.raw; + } + if (state != null) { + stateRaw = state.raw; + } + } static const typeId = 8; static const boxName = 'Orders'; @@ -32,13 +37,13 @@ class Order extends HiveObject { String transferId; @HiveField(2) - String from; + String? from; @HiveField(3) - String to; + String? to; @HiveField(4) - String stateRaw; + late String stateRaw; TradeState get state => TradeState.deserialize(raw: stateRaw); @@ -55,7 +60,7 @@ class Order extends HiveObject { String walletId; @HiveField(9) - int providerRaw; + late int providerRaw; BuyProviderDescription get provider => BuyProviderDescription.deserialize(raw: providerRaw); diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index aac8c3db6..652c92f58 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -11,12 +11,11 @@ import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; class WyreBuyProvider extends BuyProvider { - WyreBuyProvider({WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, isTestEnvironment: isTestEnvironment) { - baseApiUrl = isTestEnvironment + WyreBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : baseApiUrl = isTestEnvironment ? _baseTestApiUrl - : _baseProductApiUrl; - } + : _baseProductApiUrl, + super(wallet: wallet, isTestEnvironment: isTestEnvironment); static const _baseTestApiUrl = 'https://api.testwyre.com'; static const _baseProductApiUrl = 'https://api.sendwyre.com'; @@ -50,6 +49,7 @@ class WyreBuyProvider extends BuyProvider { final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); final url = baseApiUrl + _ordersSuffix + _reserveSuffix + _timeStampSuffix + timestamp; + final uri = Uri.parse(url); final body = { 'amount': amount, 'sourceCurrency': sourceCurrency, @@ -58,8 +58,7 @@ class WyreBuyProvider extends BuyProvider { 'referrerAccountId': _accountId, 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] }; - - final response = await post(url, + final response = await post(uri, headers: { 'Authorization': 'Bearer $_secretKey', 'Content-Type': 'application/json', @@ -89,8 +88,8 @@ class WyreBuyProvider extends BuyProvider { 'accountId': _accountId, 'country': _countryCode }; - - final response = await post(quoteUrl, + final uri = Uri.parse(quoteUrl); + final response = await post(uri, headers: { 'Authorization': 'Bearer $_secretKey', 'Content-Type': 'application/json', @@ -115,7 +114,8 @@ class WyreBuyProvider extends BuyProvider { @override Future findOrderById(String id) async { final orderUrl = baseApiUrl + _ordersSuffix + '/$id'; - final orderResponse = await get(orderUrl); + final orderUri = Uri.parse(orderUrl); + final orderResponse = await get(orderUri); if (orderResponse.statusCode != 200) { throw BuyException( @@ -136,7 +136,8 @@ class WyreBuyProvider extends BuyProvider { final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; - final transferResponse = await get(transferUrl); + final transferUri = Uri.parse(transferUrl); + final transferResponse = await get(transferUri); if (transferResponse.statusCode != 200) { throw BuyException( diff --git a/lib/core/address_label_validator.dart b/lib/core/address_label_validator.dart index 2b30272db..908e3a2e8 100644 --- a/lib/core/address_label_validator.dart +++ b/lib/core/address_label_validator.dart @@ -3,7 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/wallet_type.dart'; class AddressLabelValidator extends TextValidator { - AddressLabelValidator({WalletType type}) + AddressLabelValidator({WalletType? type}) : super( errorMessage: S.current.error_text_subaddress_name, pattern: '''^[^`,'"]{1,20}\$''', diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 9ef8d3340..e3c4ed334 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; class AddressValidator extends TextValidator { - AddressValidator({@required CryptoCurrency type}) + AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, pattern: getPattern(type), @@ -79,7 +79,7 @@ class AddressValidator extends TextValidator { } } - static List getLength(CryptoCurrency type) { + static List? getLength(CryptoCurrency type) { switch (type) { case CryptoCurrency.xmr: return null; diff --git a/lib/core/amount.dart b/lib/core/amount.dart index 62e59db18..3d64f1d66 100644 --- a/lib/core/amount.dart +++ b/lib/core/amount.dart @@ -1,37 +1,37 @@ -abstract class Amount { - Amount(this.value); +// abstract class Amount { +// Amount(this.value); - int value; +// int value; - int minorDigits; +// int minorDigits; - String code; +// String code; - String formatted(); -} +// String formatted(); +// } -class MoneroAmount extends Amount { - MoneroAmount(int value) : super(value) { - minorDigits = 12; - code = 'XMR'; - } +// class MoneroAmount extends Amount { +// MoneroAmount(int value) : super(value) { +// minorDigits = 12; +// code = 'XMR'; +// } - // const moneroAmountLength = 12; - // const moneroAmountDivider = 1000000000000; - // final moneroAmountFormat = NumberFormat() - // ..maximumFractionDigits = moneroAmountLength - // ..minimumFractionDigits = 1; +// // const moneroAmountLength = 12; +// // const moneroAmountDivider = 1000000000000; +// // final moneroAmountFormat = NumberFormat() +// // ..maximumFractionDigits = moneroAmountLength +// // ..minimumFractionDigits = 1; - // String moneroAmountToString({int amount}) => - // moneroAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)); +// // String moneroAmountToString({int amount}) => +// // moneroAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)); - // double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); +// // double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); - // int moneroParseAmount({String amount}) => moneroAmountFormat.parse(amount).toInt(); +// // int moneroParseAmount({String amount}) => moneroAmountFormat.parse(amount).toInt(); - @override - String formatted() { - // TODO: implement formatted - throw UnimplementedError(); - } -} +// @override +// String formatted() { +// // TODO: implement formatted +// throw UnimplementedError(); +// } +// } diff --git a/lib/core/amount_converter.dart b/lib/core/amount_converter.dart index b3c976e25..a11907ef2 100644 --- a/lib/core/amount_converter.dart +++ b/lib/core/amount_converter.dart @@ -47,7 +47,7 @@ class AmountConverter { case CryptoCurrency.xusd: return _moneroAmountToDouble(amount); default: - return null; + return 0.0; } } @@ -71,7 +71,7 @@ class AmountConverter { case CryptoCurrency.xusd: return _moneroParseAmount(amount); default: - return null; + return 0; } } @@ -97,11 +97,11 @@ class AmountConverter { case CryptoCurrency.xusd: return _moneroAmountToString(amount); default: - return null; + return ''; } } - static double cryptoAmountToDouble({num amount, num divider}) => + static double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider; static String _moneroAmountToString(int amount) => _moneroAmountFormat.format( diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index 52e75969f..3b14b0832 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -3,7 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/wallet_type.dart'; class AmountValidator extends TextValidator { - AmountValidator({WalletType type, bool isAutovalidate = false}) + AmountValidator({required WalletType type, bool isAutovalidate = false}) : super( errorMessage: S.current.error_text_amount, pattern: _pattern(type), diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index f2c07d265..2ae37e2b0 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -6,12 +6,12 @@ import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/encrypt.dart'; class AuthService with Store { - AuthService({this.secureStorage, this.sharedPreferences}); + AuthService({required this.secureStorage, required this.sharedPreferences}); final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; - Future setPassword(String password) async { + Future setPassword(String password) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPassword = encodedPinCode(pin: password); await secureStorage.write(key: key, value: encodedPassword); @@ -24,7 +24,7 @@ class AuthService with Store { var password = ''; try { - password = await secureStorage.read(key: key); + password = await secureStorage.read(key: key) ?? ''; } catch (e) { print(e); } @@ -35,7 +35,7 @@ class AuthService with Store { Future authenticate(String pin) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await secureStorage.read(key: key); - final decodedPin = decodedPinCode(pin: encodedPin); + final decodedPin = decodedPinCode(pin: encodedPin!); return decodedPin == pin; } diff --git a/lib/core/auth_state.dart b/lib/core/auth_state.dart index 01e4c2576..44d83eb33 100644 --- a/lib/core/auth_state.dart +++ b/lib/core/auth_state.dart @@ -7,13 +7,13 @@ class AuthenticationInProgress extends AuthState {} class AuthenticatedSuccessfully extends AuthState {} class AuthenticationFailure extends AuthState { - AuthenticationFailure({this.error}); + AuthenticationFailure({required this.error}); final String error; } class AuthenticationBanned extends AuthState { - AuthenticationBanned({this.error}); + AuthenticationBanned({required this.error}); final String error; } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 227af2d6c..850e9560d 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -20,7 +20,8 @@ import 'package:cake_wallet/wallet_types.g.dart'; class BackupService { BackupService(this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) - : _cipher = chacha20Poly1305Aead; + : _cipher = Cryptography.instance.chacha20Poly1305Aead(), + _correctWallets = []; static const currentVersion = _v1; @@ -54,7 +55,7 @@ class BackupService { case _v1: return await _exportBackupV1(password, nonce: nonce); default: - return null; + throw Exception('Incorrect version: $version for exportBackup'); } } @@ -91,8 +92,8 @@ class BackupService { }); await keychainDumpFile.writeAsBytes(keychainDump.toList()); await preferencesDumpFile.writeAsString(preferencesDump); - zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); - zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); + await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); + await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); zipEncoder.close(); final content = File(archivePath).readAsBytesSync(); @@ -103,7 +104,7 @@ class BackupService { } Future _importBackupV1(Uint8List data, String password, - {@required String nonce}) async { + {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decrypt(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -214,7 +215,7 @@ class BackupService { } Future _importKeychainDump(String password, - {@required String nonce, + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); @@ -251,11 +252,11 @@ class BackupService { } Future _exportKeychainDump(String password, - {@required String nonce, + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); - final decodedPin = decodedPinCode(pin: encodedPin); + final decodedPin = decodedPinCode(pin: encodedPin!); final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { @@ -281,41 +282,42 @@ class BackupService { } Future _exportPreferencesJSON() async { + // FIX-ME: Force unwrap final preferences = { PreferencesKey.currentWalletName: - _sharedPreferences.getString(PreferencesKey.currentWalletName), + _sharedPreferences.getString(PreferencesKey.currentWalletName)!, PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey)!, PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey), + .getInt(PreferencesKey.currentBalanceDisplayModeKey)!, PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType), + _sharedPreferences.getInt(PreferencesKey.currentWalletType)!, PreferencesKey.currentFiatCurrencyKey: - _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), + _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!, PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey), + .getBool(PreferencesKey.shouldSaveRecipientAddressKey)!, PreferencesKey.isDarkThemeLegacy: - _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), + _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy)!, PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength), + _sharedPreferences.getInt(PreferencesKey.currentPinLength)!, PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!, PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey), + .getBool(PreferencesKey.allowBiometricalAuthenticationKey)!, PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey)!, PreferencesKey.currentLanguageCode: - _sharedPreferences.getString(PreferencesKey.currentLanguageCode), + _sharedPreferences.getString(PreferencesKey.currentLanguageCode)!, PreferencesKey.displayActionListModeKey: - _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), + _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey)!, PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme), + _sharedPreferences.getInt(PreferencesKey.currentTheme)!, PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion)!, PreferencesKey.bitcoinTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), + _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!, PreferencesKey.moneroTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), + _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!, }; return json.encode(preferences); @@ -330,17 +332,26 @@ class BackupService { Future _encrypt( Uint8List data, String secretKeySource, String nonceBase64) async { - final secretKeyHash = await sha256.hash(utf8.encode(secretKeySource)); + final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); - final nonce = Nonce(base64.decode(nonceBase64)); - return await _cipher.encrypt(data, secretKey: secretKey, nonce: nonce); + final nonce = base64.decode(nonceBase64).toList(); + final box = await _cipher.encrypt(data.toList(), secretKey: secretKey, nonce: nonce); + return Uint8List.fromList(box.cipherText); } Future _decrypt( Uint8List data, String secretKeySource, String nonceBase64) async { - final secretKeyHash = await sha256.hash(utf8.encode(secretKeySource)); - final secretKey = SecretKey(secretKeyHash.bytes); - final nonce = Nonce(base64.decode(nonceBase64)); - return await _cipher.decrypt(data, secretKey: secretKey, nonce: nonce); + throw Exception('Unimplemented'); + //final secretKeyHash = await sha256.hash(utf8.encode(secretKeySource)); + //final secretKey = SecretKey(secretKeyHash.bytes); + //final nonce = Nonce(base64.decode(nonceBase64)); + //return await _cipher.decrypt(data, secretKey: secretKey, nonce: nonce); + + // final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); + // final secretKey = SecretKey(secretKeyHash.bytes); + // final nonce = base64.decode(nonceBase64).toList(); + // final box = SecretBox(data.toList(), nonce: nonce, mac: Mac); + // final plainData = await _cipher.decrypt(box, secretKey: secretKey); + // return Uint8List.fromList(plainData); } } diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 7a0513d5c..f4ec3775b 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -16,7 +16,7 @@ Future _fetchPrice(Map args) async { final fiatStringified = fiat.toString(); final uri = Uri.https(fiatApiAuthority, fiatApiPath, {'convert': fiatStringified}); - final response = await get(uri.toString()); + final response = await get(uri); if (response.statusCode != 200) { return 0.0; diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart index eafca2b0a..1fe99623e 100644 --- a/lib/core/key_service.dart +++ b/lib/core/key_service.dart @@ -7,15 +7,14 @@ class KeyService { final FlutterSecureStorage _secureStorage; - Future getWalletPassword({String walletName}) async { + Future getWalletPassword({required String walletName}) async { final key = generateStoreKeyFor( key: SecretStoreKey.moneroWalletPassword, walletName: walletName); final encodedPassword = await _secureStorage.read(key: key); - - return decodeWalletPassword(password: encodedPassword); + return decodeWalletPassword(password: encodedPassword!); } - Future saveWalletPassword({String walletName, String password}) async { + Future saveWalletPassword({required String walletName, required String password}) async { final key = generateStoreKeyFor( key: SecretStoreKey.moneroWalletPassword, walletName: walletName); final encodedPassword = encodeWalletPassword(password: password); diff --git a/lib/core/monero_account_label_validator.dart b/lib/core/monero_account_label_validator.dart index 7e287959b..ef4cf0f9a 100644 --- a/lib/core/monero_account_label_validator.dart +++ b/lib/core/monero_account_label_validator.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; class MoneroLabelValidator extends TextValidator { - MoneroLabelValidator({@required CryptoCurrency type}) + MoneroLabelValidator() : super( errorMessage: S.current.error_text_account_name, pattern: '^[a-zA-Z0-9_ ]{1,15}\$', diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index e1e5920fd..fe9a25f85 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -7,23 +7,24 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/language_list.dart'; class SeedValidator extends Validator { - SeedValidator({this.type, this.language}) - : _words = getWordList(type: type, language: language); + SeedValidator({required this.type, required this.language}) + : _words = getWordList(type: type, language: language), + super(errorMessage: 'Wrong seed mnemonic'); final WalletType type; final String language; final List _words; - static List getWordList({WalletType type, String language}) { + static List getWordList({required WalletType type, required String language}) { switch (type) { case WalletType.bitcoin: return getBitcoinWordList(language); case WalletType.litecoin: return getBitcoinWordList(language); case WalletType.monero: - return monero.getMoneroWordList(language); + return monero!.getMoneroWordList(language); case WalletType.haven: - return haven.getMoneroWordList(language); + return haven!.getMoneroWordList(language); default: return []; } @@ -31,9 +32,9 @@ class SeedValidator extends Validator { static List getBitcoinWordList(String language) { assert(language.toLowerCase() == LanguageList.english.toLowerCase()); - return bitcoin.getWordList(); + return bitcoin!.getWordList(); } @override - bool isValid(MnemonicItem value) => _words.contains(value.text); + bool isValid(MnemonicItem? value) => _words.contains(value?.text); } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index e6b04c245..e46ec2490 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -33,4 +33,6 @@ String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is LostConnectionSyncStatus) { return S.current.sync_status_failed_connect; } + + return ''; } \ No newline at end of file diff --git a/lib/core/validator.dart b/lib/core/validator.dart index 055439347..96c382b7d 100644 --- a/lib/core/validator.dart +++ b/lib/core/validator.dart @@ -1,13 +1,13 @@ import 'package:flutter/foundation.dart'; abstract class Validator { - Validator({@required this.errorMessage}); + Validator({required this.errorMessage}); final String errorMessage; - bool isValid(T value); + bool isValid(T? value); - String call(T value) => !isValid(value) ? errorMessage : null; + String? call(T? value) => !isValid(value) ? errorMessage : null; } class TextValidator extends Validator { @@ -15,28 +15,28 @@ class TextValidator extends Validator { {this.minLength, this.maxLength, this.pattern, + String errorMessage = '', this.length, - this.isAutovalidate = false, - String errorMessage}) + this.isAutovalidate = false}) : super(errorMessage: errorMessage); - final int minLength; - final int maxLength; - final List length; + final int? minLength; + final int? maxLength; + final List? length; final bool isAutovalidate; - String pattern; + String? pattern; @override - bool isValid(String value) { + bool isValid(String? value) { if (value == null || value.isEmpty) { return isAutovalidate ? true : false; } return value.length > (minLength ?? 0) && (length?.contains(value.length) ?? true) && - ((maxLength ?? 0) > 0 ? (value.length <= maxLength) : true) && + ((maxLength ?? 0) > 0 ? (value.length <= maxLength!) : true) && (pattern != null ? match(value) : true); } - bool match(String value) => RegExp(pattern).hasMatch(value); + bool match(String value) => pattern != null ? RegExp(pattern!).hasMatch(value) : false; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 1fe36f374..3b28f36c3 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -14,15 +14,13 @@ import 'package:cw_core/wallet_type.dart'; class WalletCreationService { WalletCreationService( - {WalletType initialType, - this.secureStorage, - this.keyService, - this.sharedPreferences, - this.walletInfoSource}) + {required WalletType initialType, + required this.secureStorage, + required this.keyService, + required this.sharedPreferences, + required this.walletInfoSource}) : type = initialType { - if (type != null) { - changeWalletType(type: type); - } + changeWalletType(type: type); } WalletType type; @@ -30,11 +28,11 @@ class WalletCreationService { final SharedPreferences sharedPreferences; final KeyService keyService; final Box walletInfoSource; - WalletService _service; + WalletService? _service; static const _isNewMoneroWalletPasswordUpdated = true; - void changeWalletType({@required WalletType type}) { + void changeWalletType({required WalletType type}) { this.type = type; _service = getIt.get(param1: type); } @@ -64,7 +62,7 @@ class WalletCreationService { credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - final wallet = await _service.create(credentials); + final wallet = await _service!.create(credentials); if (wallet.type == WalletType.monero) { await sharedPreferences @@ -82,7 +80,7 @@ class WalletCreationService { credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - final wallet = await _service.restoreFromKeys(credentials); + final wallet = await _service!.restoreFromKeys(credentials); if (wallet.type == WalletType.monero) { await sharedPreferences @@ -100,7 +98,7 @@ class WalletCreationService { credentials.password = password; await keyService.saveWalletPassword( password: password, walletName: credentials.name); - final wallet = await _service.restoreFromSeed(credentials); + final wallet = await _service!.restoreFromSeed(credentials); if (wallet.type == WalletType.monero) { await sharedPreferences diff --git a/lib/core/wallet_creation_state.dart b/lib/core/wallet_creation_state.dart index c732a1899..728bc477f 100644 --- a/lib/core/wallet_creation_state.dart +++ b/lib/core/wallet_creation_state.dart @@ -7,7 +7,7 @@ class WalletCreating extends WalletCreationState {} class WalletCreatedSuccessfully extends WalletCreationState {} class WalletCreationFailure extends WalletCreationState { - WalletCreationFailure({@required this.error}); + WalletCreationFailure({required this.error}); final String error; } \ No newline at end of file diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 844e5c1ca..5bae5b346 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -17,18 +17,15 @@ class WalletLoadingService { final WalletService Function(WalletType type) walletServiceFactory; Future load(WalletType type, String name) async { - if (walletServiceFactory == null) { - throw Exception('WalletLoadingService.walletServiceFactory is not set'); - } - final walletService = walletServiceFactory?.call(type); + final walletService = walletServiceFactory.call(type); final password = await keyService.getWalletPassword(walletName: name); - final wallet = await walletService.openWallet(name, password); + final wallet = await walletService.openWallet(name, password); - if (type == WalletType.monero) { - await upateMoneroWalletPassword(wallet); - } + if (type == WalletType.monero) { + await upateMoneroWalletPassword(wallet); + } - return wallet; + return wallet; } Future upateMoneroWalletPassword(WalletBase wallet) async { diff --git a/lib/di.dart b/lib/di.dart index 5cc698d27..5a2eb7285 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -156,26 +156,26 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; final getIt = GetIt.instance; var _isSetupFinished = false; -Box _walletInfoSource; -Box _nodeSource; -Box _contactSource; -Box _tradesSource; -Box