From 7662598f78eefdcd905bfaf5a718a41a7c65ec79 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Aug 2022 11:40:16 +0100 Subject: [PATCH 001/110] 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 002/110] 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 003/110] 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 004/110] 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 005/110] 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 006/110] 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 007/110] 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 008/110] 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 009/110] 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 010/110] 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 011/110] 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 012/110] 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 013/110] 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 014/110] 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 015/110] 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 016/110] 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 017/110] 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 018/110] 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 019/110] 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 020/110] 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 021/110] 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 022/110] 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 023/110] 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 024/110] 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 025/110] 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 026/110] =?UTF-8?q?=D0=A1ake=20pay=20filter=20white=20scre?= =?UTF-8?q?en=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 027/110] 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 028/110] 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 029/110] 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 030/110] 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 031/110] 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 032/110] 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 033/110] 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 034/110] 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 035/110] 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 036/110] 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 037/110] 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 038/110] 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 039/110] 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 040/110] 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 041/110] 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 042/110] 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 043/110] 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 044/110] 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 045/110] 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 046/110] 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