From 6c0401089a994e1325c01dfd17a9027c9e2fcb21 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer <justin.ehrenhofer@gmail.com> Date: Fri, 7 Apr 2023 08:54:39 -0500 Subject: [PATCH 01/28] Improve sending error messages Ideally these should be moved to the translation strings --- .../lib/bitcoin_transaction_wrong_balance_exception.dart | 2 +- cw_haven/lib/haven_wallet.dart | 6 +++--- cw_monero/lib/monero_wallet.dart | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart index 9960d6c65..3f379bea0 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart @@ -6,5 +6,5 @@ class BitcoinTransactionWrongBalanceException implements Exception { final CryptoCurrency currency; @override - String toString() => 'Wrong balance. Not enough ${currency.title} on your balance.'; + String toString() => 'You do not have enough ${currency.title} to send this amount.'; } \ No newline at end of file diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index a4b949d8f..e761d21fa 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -166,14 +166,14 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, if (hasMultiDestination) { if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); } final int totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); } final moneroOutputs = outputs.map((output) => @@ -204,7 +204,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, final formattedBalance = moneroAmountToString(amount: unlockedBalance); throw HavenTransactionCreationException( - 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); + 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } pendingTransactionDescription = diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index a9d708c05..cb9cb0ceb 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -180,14 +180,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, if (hasMultiDestination) { if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } final int totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } final moneroOutputs = outputs.map((output) { @@ -222,7 +222,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, final formattedBalance = moneroAmountToString(amount: unlockedBalance); throw MoneroTransactionCreationException( - 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); + 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } pendingTransactionDescription = From 4fd8b722e97b5e3eb064fa55603cbc5cf87a80e2 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:53:01 +0300 Subject: [PATCH 02/28] Add padding to adjust top (#861) * Add padding to adjust top * Update padding --- lib/src/screens/exchange/exchange_template_page.dart | 2 +- lib/src/screens/receive/anonpay_invoice_page.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 67b4b3605..cacc52498 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -132,7 +132,7 @@ class ExchangeTemplatePage extends BasePage { begin: Alignment.topLeft, end: Alignment.bottomRight), ), - padding: EdgeInsets.fromLTRB(24, 90, 24, 32), + padding: EdgeInsets.fromLTRB(24, 100, 24, 32), child: Observer( builder: (_) => ExchangeCard( amountFocusNode: _depositAmountFocus, diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index 1ba2f6122..91c9aaef1 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -99,7 +99,7 @@ class AnonPayInvoicePage extends BasePage { ), child: Observer(builder: (_) { return Padding( - padding: EdgeInsets.fromLTRB(24, 100, 24, 0), + padding: EdgeInsets.fromLTRB(24, 120, 24, 0), child: AnonInvoiceForm( nameController: _nameController, descriptionController: _descriptionController, From 28c80fe69925ecc27fc71451cd7d09c2cb124d91 Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Mon, 10 Apr 2023 20:10:35 +0300 Subject: [PATCH 03/28] Hide app screen from switcher (#870) --- .../java/com/cakewallet/cake_wallet/MainActivity.java | 9 ++++++++- ios/Runner/AppDelegate.swift | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 141c26944..19373aab2 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -11,6 +11,7 @@ import io.flutter.plugin.common.MethodChannel; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.WindowManager; @@ -56,7 +57,7 @@ public class MainActivity extends FlutterFragmentActivity { handler.post(() -> result.success(bytes)); break; case "getUnstoppableDomainAddress": - int version = Build.VERSION.SDK_INT; + int version = Build.VERSION.SDK_INT; if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { getUnstoppableDomainAddress(call, result); } else { @@ -87,4 +88,10 @@ public class MainActivity extends FlutterFragmentActivity { } }); } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } } \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c4d460e08..e3f3da418 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -110,4 +110,12 @@ import UnstoppableDomainsResolution GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + override func applicationWillResignActive(_: UIApplication ) { + self.window?.isHidden = true; + } + + override func applicationDidBecomeActive(_: UIApplication) { + self.window?.isHidden = false; + } } From 613ca9d8151b219345e878e7104dc2024fd1f79e Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Tue, 11 Apr 2023 16:58:33 +0200 Subject: [PATCH 04/28] Fixate device info package version (#875) --- pubspec_base.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 61282761c..7825824a9 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -62,7 +62,7 @@ dependencies: device_display_brightness: ^0.0.6 platform_device_id: ^1.0.1 flutter_mailer: ^2.0.2 - device_info_plus: ^8.1.0 + device_info_plus: 8.1.0 cake_backup: git: url: https://github.com/cake-tech/cake_backup.git From 5c4fd789519dd248d7abb5dda88af96069b7a6b2 Mon Sep 17 00:00:00 2001 From: Mathias Herberts <Mathias.Herberts+git@gmail.com> Date: Tue, 11 Apr 2023 17:12:48 +0200 Subject: [PATCH 05/28] French translation fixes post 4.6.2 (#871) * Minor French translation fixes * Minor fixes to French translation --- res/values/strings_fr.arb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f62096c76..9ba3066c1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -9,6 +9,8 @@ "monero_com": "Monero.com par Cake Wallet", "monero_com_wallet_text": "Super portefeuille (wallet) pour Monero", + "haven_app": "Haven par Cake Wallet", + "haven_app_wallet_text": "Super portefeuille (wallet) pour Haven", "accounts" : "Comptes", "edit" : "Modifier", @@ -483,7 +485,7 @@ "submit_request" : "soumettre une requête", - "buy_alert_content" : "Actuellement, nous ne prenons en charge que l'achat de Bitcoin, Litecoin et Monero. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Litecoin ou Monero.", + "buy_alert_content" : "Actuellement, nous ne prenons en charge que l'achat de Bitcoin, Litecoin et Monero. Veuillez créer ou basculer vers votre portefeuille (wallet) Bitcoin, Litecoin ou Monero.", "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 (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.", @@ -548,7 +550,6 @@ "sign_up": "S'inscrire", "forgot_password": "Mot de passe oublié", "reset_password": "Réinitialiser le mot de passe", - "manage_cards": "Cartes cadeaux", "setup_your_debit_card": "Configurer votre carte de débit", "no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où", "how_to_use_card": "Comment utiliser cette carte", @@ -561,7 +562,7 @@ "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 peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", + "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 (wallet) numérique peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", "please_reference_document": "Veuillez vous référer aux documents ci-dessous pour plus d'informations.", "cardholder_agreement": "Contrat de titulaire de carte", "e_sign_consent": "Consentement de signature électronique", @@ -597,7 +598,7 @@ "tip": "Pourboire :", "custom": "personnalisé", "by_cake_pay": "par Cake Pay", - "expire": "Expire", + "expires": "Expire", "mm": "MM", "yy": "AA", "online": "En ligne", @@ -633,6 +634,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": "Cartes-Cadeaux", "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 utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", @@ -650,7 +652,7 @@ "low_fee": "Frais modiques", "low_fee_alert": "Vous utilisez actuellement une priorité de frais de réseau peu élevés. Cela pourrait entraîner de longues attentes, des taux différents ou des transactions annulées. Nous vous recommandons de fixer des frais plus élevés pour une meilleure expérience.", "ignor": "Ignorer", - "use_suggested": "Utilisation suggérée", + "use_suggested": "Suivre la suggestion", "do_not_share_warning_text" : "Ne les partagez avec personne, y compris avec l'assistance.\n\nVos fonds seraient inmanquablement volés !", "help": "aide", "all_transactions": "Toutes transactions", @@ -678,22 +680,22 @@ "orbot_running_alert": "Veuillez vous assurer qu'Orbot est en cours d'exécution avant de vous connecter à ce nœud.", "contact_list_contacts": "Contacts", "contact_list_wallets": "Mes portefeuilles (wallets)", - "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience! Vous serez averti par e-mail lorsque le paiement sera confirmé.", - "send_to_this_address" : "Envoyez ${currency} ${tag}à cette adresse", + "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience ! Vous serez averti par e-mail lorsque le paiement sera confirmé.", + "send_to_this_address" : "Envoyer ${currency} ${tag}à cette adresse", "arrive_in_this_address" : "${currency} ${tag}arrivera à cette adresse", - "do_not_send": "N'envoyez pas", - "error_dialog_content": "Oups, nous avons eu une erreur.\n\nVeuillez envoyer le rapport de plantage à notre équipe d'assistance pour améliorer l'application.", + "do_not_send": "Ne pas envoyer", + "error_dialog_content": "Oups, nous avons rencontré une erreur.\n\nMerci d'envoyer le rapport d'erreur à notre équipe d'assistance afin de nous permettre d'améliorer l'application.", "decimal_places_error": "Trop de décimales", "edit_node": "Modifier le nœud", "invoice_details": "Détails de la facture", "donation_link_details": "Détails du lien de don", - "anonpay_description": "Générez ${type}. Le destinataire peut ${method} avec n'importe quelle crypto-monnaie prise en charge, et vous recevrez des fonds dans ce portefeuille.", + "anonpay_description": "Générez ${type}. Le destinataire peut ${method} avec n'importe quelle crypto-monnaie prise en charge, et vous recevrez des fonds dans ce portefeuille (wallet).", "create_invoice": "Créer une facture", "create_donation_link": "Créer un lien de don", "optional_email_hint": "E-mail de notification du bénéficiaire facultatif", "optional_description": "Descriptif facultatif", "optional_name": "Nom du destinataire facultatif", "clearnet_link": "Lien Clearnet", - "onion_link": "Lien d'oignon", + "onion_link": "Lien .onion", "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge" } From c5477e4f9ea8a0d4d9ad77db2171999664a8b0c4 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Fri, 14 Apr 2023 06:39:08 +0200 Subject: [PATCH 06/28] Dashboard desktop view (#737) * Add build scripts for macOS. Add macos for cw_monero plugin. Add macos proj to the application. * - Update Flutter secure storage to work with macos - Enable uni links only on Mobile - Update devcelocale to work with macos * Add network access to mac * Change Dashboard view on desktop size screens * Add on Tap to desktop_action_button.dart Remove unused functions * Fix arch match for monero lib for darwin x86_64 -> x86-64 * Add Bundle ID in entitlements files through app config script * Update deployment target to 10.13 * Revert back to Cake fork for secure storage * Revert back to Cake fork for secure storage * Revert mac os version * Revert mac os version * Add platform channel specific code for mac os * Add desktop sidebar * [skip ci] Add desktop sidebar * [skip ci] Add desktop sidebar * - Remove legacy migration from macos - Remove wake lock native code and just use the ready made package * Remove wake lock native code and just use the ready made package * Remove unstoppable domain from macos since it's not supported * Temporarily fetch unstoppable domains only on mobile * refactor desktop settings sidebar * Ignore increasing brightness for non-mobile platforms * Add Wallet selection dropdown to dashboard desktop view * Generate MacOS icons * localize settings * fix dashboard sidebar and responsive utils * Change Mac os app name and bundle id * Fix exchange page as fullScreenDialog * Remove constants * - Refactor onRamper to have a single point of modification - Enlarge initial app size - update Flutter and Packages * Add pubspec.lock and Podfile.lock to gitignore * Remove Podfile.lock from cache * Fix bug on sidebar reset * Fix issues from code review * [skip ci] reformat desktop dashboard * [skip ci] reformat desktop dashboard * Revert removing .lock files * Revert changes in .gitignore * [skip ci] remove .project changes * [skip ci] remove .project changes * Separate Dashboard desktop view from mobile view * constraint images and pincoded box * Remove drawer from mac os * - Listen to keyboard events in PIN screen - Fix PIN buttons style * Fix desktop nav bar UI * Add Marketplace to dashboard view * Update trailing icon to open transaction page * Update widget contraints * Add empty trailing to center page title on desktop * Refresh desktop dashboard actions on wallet change * Change ionia welcome page animation * Fix Constrained width screens UI * Refactor sidebar state management * remove empty line * Add max width constrain to Welcome page * Change Exchange page UI depending on platform * - Change design/paddings for Send page on desktop view - Make AddTemplateButton instead of having it duplicated in send/exchange * Fix Desktop dashboard actions background color * Constrain primary Buttons width * Make side menu items toggle back to dashboard * Add padding to support page * Add width constraints to desktop dashboard * Fix UI issues, paddings and alignments * Rename misleading variable Change initial mac window size * Fix wallet create in settings * remove unnecessary code * remove unnecessary code * Remove duplicated constrains * - Use close icon on main screens - Minor UI fixes * fix pageview controller reset index * Add create and restore wallet options to dropdown menu * Fix desktop background color and address book view issues * Fix input field * Add onFieldSubmitted to allow "enter" button interaction * Fix issue from code review * Fix Popup width constraint and add focus orders * Fix variable name * Fix issues from code review * refactor dropdown items * Fix alignment in create and restore wallet screens * Fix dropdown change state bug Hide scanner for desktop * remove space * override navbar with desktopnavbar * Remove autofocus * remove unused code * Fix ionia input field alignment * Replace removed code * Add app lock feature on mac * Add assertion to avoid null * Add Nano currency image * Enable adding contact from send screen * Fix UI issues Add missing translation * pop only PIN screen after successful auth * Add back wallet settings page to desktop settings actions * Fix Navigation animation for settings screens * Fixate MobX version to fix restore issue * CW-324 Refresh current settings page if wallet changed (#811) * Fix refresh current settings page if wallet changed * Fix refresh current settings page if wallet changed * Refresh Wallet Seeds/Keys List upon wallet change --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> * Remove navigation workaround for duplicate key, and fix the issue by handling creation/disposing of global key (#840) * Cw 323 add wallet list to settings on mac (#843) * Remove navigation workaround for duplicate key, and fix the issue by handling creation/disposing of global key * - Register Wallet List as singleton in Desktop to be modify the same instance from settings and dropdown - General Fixes and Enhancements * Fix Changing/Restoring wallet from settings * Fix Create wallet not showing seeds screens if launched from settings * Add max width constraint for Alerts * - Add Desktop API keys - Fix Change back up password issue - Fix Popup width * Sync Mac with latest main updates * Swap Transactions icon with lock icon * Save backup file locally on desktop * Sync with latest main updates * Fix Navigation issues with anonpay * Update macos build version * Remove deprecated custom wake lock code for Android * Remove Legacy CryptoSwift package from MacOS * - Refactor Payfura page code - Add OnRamper new configs to onramper_buy_provider.dart - Fix Conflicts with main * Updated device locale package * Update android tools * Revert changes and update only gradle version * Downgrade android tools version * Update gradle version * Update package/gradle/plugin version * - Fixate device locale version - Downgrade gradle version * Update kotlin version * Update gradle version * Trial for a custom fork from devicelocale * Fixate shared preferences package version * Revert gradle version * Revert kotlin version * Downgrade gradle version * Downgrade gradle version * Repair cache and clean before build * Fixate flutter version * update google services version * revert google services version * Force shared pref android version * Override shared prefs android package version * Override shared prefs android package [skip ci] --------- Co-authored-by: M <m@cakewallet.com> Co-authored-by: Godwin Asuquo <godilite@gmail.com> Co-authored-by: Godwin Asuquo <41484542+godilite@users.noreply.github.com> --- .github/workflows/pr_test_build.yml | 2 + .gitignore | 5 + .metadata | 24 +- .../cakewallet/cake_wallet/MainActivity.java | 8 - .../com/cakewallet/haven/MainActivity.java | 8 - .../java/com/monero/app/MainActivity.java | 8 - .../desktop_transactions_outline_icon.png | Bin 0 -> 663 bytes .../desktop_transactions_solid_icon.png | Bin 0 -> 390 bytes assets/images/settings_outline.png | Bin 0 -> 871 bytes assets/images/support_icon.png | Bin 0 -> 819 bytes assets/images/wallet_outline.png | Bin 0 -> 628 bytes assets/images/wallet_solid.png | Bin 0 -> 581 bytes cw_monero/.gitignore | 5 +- cw_monero/.metadata | 26 +- cw_monero/analysis_options.yaml | 4 + cw_monero/example/.gitignore | 44 + cw_monero/example/README.md | 16 + cw_monero/example/analysis_options.yaml | 29 + cw_monero/example/lib/main.dart | 63 ++ cw_monero/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 14 + cw_monero/example/macos/Podfile | 40 + cw_monero/example/macos/Podfile.lock | 22 + .../macos/Runner.xcodeproj/project.pbxproj | 632 ++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + cw_monero/example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + cw_monero/example/pubspec.lock | 403 +++++++++ cw_monero/example/pubspec.yaml | 84 ++ cw_monero/example/test/widget_test.dart | 27 + cw_monero/lib/cw_monero.dart | 8 + cw_monero/lib/cw_monero_method_channel.dart | 17 + .../lib/cw_monero_platform_interface.dart | 29 + cw_monero/macos/Classes/CwMoneroPlugin.swift | 19 + cw_monero/macos/Classes/CwWalletListener.h | 23 + cw_monero/macos/Classes/monero_api.cpp | 798 ++++++++++++++++++ cw_monero/macos/Classes/monero_api.h | 38 + cw_monero/macos/cw_monero_base.podspec | 56 ++ cw_monero/pubspec.yaml | 11 +- .../test/cw_monero_method_channel_test.dart | 24 + cw_monero/test/cw_monero_test.dart | 29 + ios/CakeWallet/wakeLock.swift | 20 - ios/Runner.xcodeproj/project.pbxproj | 2 - ios/Runner/AppDelegate.swift | 5 - lib/buy/moonpay/moonpay_buy_provider.dart | 6 +- lib/buy/onramper/onramper_buy_provider.dart | 69 ++ lib/buy/payfura/payfura_buy_provider.dart | 24 + lib/di.dart | 134 ++- lib/entities/desktop_dropdown_item.dart | 9 + lib/entities/main_actions.dart | 142 ++++ lib/entities/unstoppable_domain_address.dart | 20 +- lib/entities/wake_lock.dart | 21 - .../changenow_exchange_provider.dart | 3 +- .../simpleswap_exchange_provider.dart | 3 +- .../on_authentication_state_change.dart | 4 +- .../on_wallet_sync_status_change.dart | 9 +- lib/router.dart | 50 +- lib/routes.dart | 4 + lib/src/screens/backup/backup_page.dart | 33 +- .../backup/edit_backup_password_page.dart | 3 +- lib/src/screens/base_page.dart | 25 +- lib/src/screens/buy/onramper_page.dart | 44 +- lib/src/screens/buy/payfura_page.dart | 29 +- .../screens/contact/contact_list_page.dart | 4 - lib/src/screens/dashboard/dashboard_page.dart | 312 +++---- .../dashboard/desktop_dashboard_page.dart | 111 +++ .../desktop_action_button.dart | 69 ++ .../desktop_dashboard_actions.dart | 80 ++ .../desktop_dashboard_navbar.dart | 48 ++ .../desktop_sidebar/side_menu.dart | 32 + .../desktop_sidebar/side_menu_item.dart | 50 ++ .../desktop_sidebar_wrapper.dart | 175 ++++ .../desktop_wallet_selection_dropdown.dart | 199 +++++ .../desktop_widgets/dropdown_item_widget.dart | 36 + lib/src/screens/dashboard/wallet_menu.dart | 74 -- .../screens/dashboard/wallet_menu_item.dart | 12 - .../dashboard/widgets/address_page.dart | 37 +- .../dashboard/widgets/balance_page.dart | 9 +- .../dashboard/widgets/menu_widget.dart | 151 +--- .../present_receive_option_picker.dart | 6 +- .../dashboard/widgets/transactions_page.dart | 1 + lib/src/screens/exchange/exchange_page.dart | 417 ++++----- .../exchange/exchange_template_page.dart | 190 ++--- .../exchange/widgets/currency_picker.dart | 17 +- .../desktop_exchange_cards_section.dart | 31 + .../exchange/widgets/exchange_card.dart | 207 +++-- .../mobile_exchange_cards_section.dart | 58 ++ .../ionia/auth/ionia_create_account_page.dart | 15 +- .../screens/ionia/auth/ionia_login_page.dart | 15 +- .../ionia/auth/ionia_verify_otp_page.dart | 5 +- .../ionia/cards/ionia_buy_gift_card.dart | 65 +- .../screens/new_wallet/new_wallet_page.dart | 225 +++-- .../new_wallet/new_wallet_type_page.dart | 95 +-- .../nodes/node_create_or_edit_page.dart | 1 - lib/src/screens/pin_code/pin_code_widget.dart | 357 ++++---- .../screens/receive/anonpay_invoice_page.dart | 6 +- .../screens/receive/widgets/qr_widget.dart | 74 +- .../restore/restore_from_backup_page.dart | 80 +- .../screens/restore/restore_options_page.dart | 53 +- .../screens/restore/wallet_restore_page.dart | 242 +++--- lib/src/screens/root/root.dart | 5 +- lib/src/screens/seed/pre_seed_page.dart | 77 +- lib/src/screens/seed/wallet_seed_page.dart | 196 ++--- lib/src/screens/send/send_page.dart | 321 ++++--- lib/src/screens/send/widgets/send_card.dart | 12 +- .../desktop_settings_page.dart | 105 +++ .../settings/display_settings_page.dart | 17 +- lib/src/screens/support/support_page.dart | 51 +- .../screens/wallet_list/wallet_list_page.dart | 124 ++- lib/src/screens/welcome/welcome_page.dart | 256 +++--- lib/src/widgets/add_template_button.dart | 52 ++ lib/src/widgets/address_text_field.dart | 33 +- lib/src/widgets/alert_background.dart | 11 +- lib/src/widgets/alert_close_button.dart | 5 +- lib/src/widgets/base_text_form_field.dart | 11 +- lib/src/widgets/check_box_picker.dart | 100 +-- lib/src/widgets/market_place_item.dart | 3 + lib/src/widgets/nav_bar.dart | 37 +- lib/src/widgets/picker.dart | 183 ++-- lib/src/widgets/primary_button.dart | 229 ++--- lib/src/widgets/setting_action_button.dart | 81 ++ lib/src/widgets/setting_actions.dart | 109 +++ lib/store/settings_store.dart | 6 +- lib/utils/device_info.dart | 11 + lib/utils/exception_handler.dart | 1 + lib/utils/responsive_layout_util.dart | 34 + .../dashboard/dashboard_view_model.dart | 8 - .../dashboard/desktop_sidebar_view_model.dart | 34 + .../node_list/node_list_view_model.dart | 40 +- lib/view_model/wallet_keys_view_model.dart | 102 ++- .../wallet_list/wallet_list_item.dart | 14 +- .../wallet_list/wallet_list_view_model.dart | 1 + macos/.gitignore | 7 + macos/CakeWallet/secRandom.swift | 12 + macos/Flutter/Flutter-Debug.xcconfig | 2 + macos/Flutter/Flutter-Release.xcconfig | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 36 + macos/Podfile | 40 + macos/Podfile.lock | 118 +++ macos/Runner.xcodeproj/project.pbxproj | 663 +++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + macos/Runner.xcodeproj/project_base.pbxproj | 636 ++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 87 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + macos/Runner/AppDelegate.swift | 33 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 112502 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 7683 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 678 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 17249 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1522 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 42348 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 3520 bytes macos/Runner/Base.lproj/MainMenu.xib | 344 ++++++++ macos/Runner/Configs/AppInfo.xcconfig | 14 + macos/Runner/Configs/Debug.xcconfig | 2 + macos/Runner/Configs/Release.xcconfig | 2 + macos/Runner/Configs/Warnings.xcconfig | 13 + macos/Runner/DebugProfileBase.entitlements | 18 + macos/Runner/InfoBase.plist | 34 + macos/Runner/MainFlutterWindow.swift | 15 + macos/Runner/ReleaseBase.entitlements | 14 + pubspec_base.yaml | 13 +- res/values/strings_ar.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_zh.arb | 1 + scripts/macos/app_config.sh | 34 + scripts/macos/app_env.sh | 39 + scripts/macos/build_all.sh | 3 + scripts/macos/build_boost_arm64.sh | 4 + scripts/macos/build_boost_common.sh | 210 +++++ scripts/macos/build_boost_universal.sh | 4 + scripts/macos/build_boost_x86_64.sh | 4 + scripts/macos/build_expat.sh | 17 + scripts/macos/build_haven.sh | 50 ++ scripts/macos/build_monero.sh | 54 ++ scripts/macos/build_monero_all.sh | 20 + scripts/macos/build_openssl_arm64.sh | 4 + scripts/macos/build_openssl_common.sh | 116 +++ scripts/macos/build_openssl_universal.sh | 4 + scripts/macos/build_openssl_x86_64.sh | 4 + scripts/macos/build_sodium.sh | 16 + scripts/macos/build_unbound.sh | 23 + scripts/macos/build_zmq.sh | 17 + scripts/macos/cakewallet.sh | 4 + scripts/macos/config.sh | 13 + scripts/macos/gen_arm64.sh | 5 + scripts/macos/gen_common.sh | 31 + scripts/macos/gen_universal.sh | 5 + scripts/macos/gen_x86_64.sh | 5 + scripts/macos/setup.sh | 40 + tool/utils/secret_key.dart | 2 + 231 files changed, 9946 insertions(+), 2653 deletions(-) create mode 100644 assets/images/desktop_transactions_outline_icon.png create mode 100644 assets/images/desktop_transactions_solid_icon.png create mode 100644 assets/images/settings_outline.png create mode 100644 assets/images/support_icon.png create mode 100644 assets/images/wallet_outline.png create mode 100644 assets/images/wallet_solid.png create mode 100644 cw_monero/analysis_options.yaml create mode 100644 cw_monero/example/.gitignore create mode 100644 cw_monero/example/README.md create mode 100644 cw_monero/example/analysis_options.yaml create mode 100644 cw_monero/example/lib/main.dart create mode 100644 cw_monero/example/macos/.gitignore create mode 100644 cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 cw_monero/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 cw_monero/example/macos/Podfile create mode 100644 cw_monero/example/macos/Podfile.lock create mode 100644 cw_monero/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 cw_monero/example/macos/Runner/AppDelegate.swift create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 cw_monero/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 cw_monero/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 cw_monero/example/macos/Runner/Configs/Release.xcconfig create mode 100644 cw_monero/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 cw_monero/example/macos/Runner/DebugProfile.entitlements create mode 100644 cw_monero/example/macos/Runner/Info.plist create mode 100644 cw_monero/example/macos/Runner/MainFlutterWindow.swift create mode 100644 cw_monero/example/macos/Runner/Release.entitlements create mode 100644 cw_monero/example/pubspec.lock create mode 100644 cw_monero/example/pubspec.yaml create mode 100644 cw_monero/example/test/widget_test.dart create mode 100644 cw_monero/lib/cw_monero.dart create mode 100644 cw_monero/lib/cw_monero_method_channel.dart create mode 100644 cw_monero/lib/cw_monero_platform_interface.dart create mode 100644 cw_monero/macos/Classes/CwMoneroPlugin.swift create mode 100644 cw_monero/macos/Classes/CwWalletListener.h create mode 100644 cw_monero/macos/Classes/monero_api.cpp create mode 100644 cw_monero/macos/Classes/monero_api.h create mode 100644 cw_monero/macos/cw_monero_base.podspec create mode 100644 cw_monero/test/cw_monero_method_channel_test.dart create mode 100644 cw_monero/test/cw_monero_test.dart delete mode 100644 ios/CakeWallet/wakeLock.swift create mode 100644 lib/buy/onramper/onramper_buy_provider.dart create mode 100644 lib/buy/payfura/payfura_buy_provider.dart create mode 100644 lib/entities/desktop_dropdown_item.dart create mode 100644 lib/entities/main_actions.dart delete mode 100644 lib/entities/wake_lock.dart create mode 100644 lib/src/screens/dashboard/desktop_dashboard_page.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart create mode 100644 lib/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart delete mode 100644 lib/src/screens/dashboard/wallet_menu.dart delete mode 100644 lib/src/screens/dashboard/wallet_menu_item.dart create mode 100644 lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart create mode 100644 lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart create mode 100644 lib/src/screens/settings/desktop_settings/desktop_settings_page.dart create mode 100644 lib/src/widgets/add_template_button.dart create mode 100644 lib/src/widgets/setting_action_button.dart create mode 100644 lib/src/widgets/setting_actions.dart create mode 100644 lib/utils/device_info.dart create mode 100644 lib/utils/responsive_layout_util.dart create mode 100644 lib/view_model/dashboard/desktop_sidebar_view_model.dart create mode 100644 macos/.gitignore create mode 100644 macos/CakeWallet/secRandom.swift create mode 100644 macos/Flutter/Flutter-Debug.xcconfig create mode 100644 macos/Flutter/Flutter-Release.xcconfig create mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock create mode 100644 macos/Runner.xcodeproj/project.pbxproj create mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner.xcodeproj/project_base.pbxproj create mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 macos/Runner/AppDelegate.swift create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 macos/Runner/Base.lproj/MainMenu.xib create mode 100644 macos/Runner/Configs/AppInfo.xcconfig create mode 100644 macos/Runner/Configs/Debug.xcconfig create mode 100644 macos/Runner/Configs/Release.xcconfig create mode 100644 macos/Runner/Configs/Warnings.xcconfig create mode 100644 macos/Runner/DebugProfileBase.entitlements create mode 100644 macos/Runner/InfoBase.plist create mode 100644 macos/Runner/MainFlutterWindow.swift create mode 100644 macos/Runner/ReleaseBase.entitlements create mode 100755 scripts/macos/app_config.sh create mode 100755 scripts/macos/app_env.sh create mode 100755 scripts/macos/build_all.sh create mode 100755 scripts/macos/build_boost_arm64.sh create mode 100755 scripts/macos/build_boost_common.sh create mode 100755 scripts/macos/build_boost_universal.sh create mode 100755 scripts/macos/build_boost_x86_64.sh create mode 100755 scripts/macos/build_expat.sh create mode 100755 scripts/macos/build_haven.sh create mode 100755 scripts/macos/build_monero.sh create mode 100755 scripts/macos/build_monero_all.sh create mode 100755 scripts/macos/build_openssl_arm64.sh create mode 100755 scripts/macos/build_openssl_common.sh create mode 100755 scripts/macos/build_openssl_universal.sh create mode 100755 scripts/macos/build_openssl_x86_64.sh create mode 100755 scripts/macos/build_sodium.sh create mode 100755 scripts/macos/build_unbound.sh create mode 100755 scripts/macos/build_zmq.sh create mode 100755 scripts/macos/cakewallet.sh create mode 100755 scripts/macos/config.sh create mode 100755 scripts/macos/gen_arm64.sh create mode 100755 scripts/macos/gen_common.sh create mode 100755 scripts/macos/gen_universal.sh create mode 100755 scripts/macos/gen_x86_64.sh create mode 100755 scripts/macos/setup.sh diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index fc8f9b8ae..076fc2ea1 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -99,6 +99,7 @@ jobs: echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -107,6 +108,7 @@ jobs: echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const sideShiftApiKey = '${{ secrets.SIDE_SHIFT_API_KEY }}';" >> lib/.secrets.g.dart echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart diff --git a/.gitignore b/.gitignore index 7e3f38beb..9fb7fd204 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,8 @@ assets/images/app_logo.png /pubspec.lock /android/app.properties /android/app/src/main/AndroidManifest.xml + + +macos/Runner/Info.plist +macos/Runner/DebugProfile.entitlements +macos/Runner/Release.entitlements \ No newline at end of file diff --git a/.metadata b/.metadata index e0236519d..cdddb9350 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 20e59316b8b8474554b38493b8ca888794b0234a + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: macos + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 19373aab2..1c1e42df8 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -41,14 +41,6 @@ public class MainActivity extends FlutterFragmentActivity { try { switch (call.method) { - case "enableWakeScreen": - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; - case "disableWakeScreen": - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; case "sec_random": int count = call.argument("count"); SecureRandom random = new SecureRandom(); diff --git a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java index 065af9318..8c13d1f8d 100644 --- a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java @@ -40,14 +40,6 @@ public class MainActivity extends FlutterFragmentActivity { try { switch (call.method) { - case "enableWakeScreen": - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; - case "disableWakeScreen": - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; case "sec_random": int count = call.argument("count"); SecureRandom random = new SecureRandom(); diff --git a/android/app/src/main/java/com/monero/app/MainActivity.java b/android/app/src/main/java/com/monero/app/MainActivity.java index 385932b38..f9e4f0882 100644 --- a/android/app/src/main/java/com/monero/app/MainActivity.java +++ b/android/app/src/main/java/com/monero/app/MainActivity.java @@ -40,14 +40,6 @@ public class MainActivity extends FlutterFragmentActivity { try { switch (call.method) { - case "enableWakeScreen": - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; - case "disableWakeScreen": - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - handler.post(() -> result.success(true)); - break; case "sec_random": int count = call.argument("count"); SecureRandom random = new SecureRandom(); diff --git a/assets/images/desktop_transactions_outline_icon.png b/assets/images/desktop_transactions_outline_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38fc3886664e5e6180383934c43a8676d2770eeb GIT binary patch literal 663 zcmV;I0%-k-P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0xU^HK~#7F?NwcF z(?AfOSqF$jE145uPC$-8UXTnAB{v`^AW@-$swsw6fQo2xgTM(wA7~ISDK`i=@DUvK zfdcUk-`KU|($Yq$ANxt(?Ao*6xAU_TqAOegE-qTZz+GOx@ffe~;&F+^ori>kBO)7O z^5f5MbMWqkAD$9e-}zQ^96N#$NI4S02BRD2fEAb7c<EjW<JdOdtUVm(0?*$Ly=&KL zlpMxVbNiWpKnoziA!8>VsP~8F=bc`=5gGje@_YF4RC!VIL}2aySP!V4+?;Bti4p-3 zscBd;w=RS9fE*d4c{~<irrRPEQb}OVAJHwHS9B0&7s>ECyTcKc01@^uM7cozfyzO# z_A3S1@%E};rxMWlu-~%suRw{|T7A&V<_im?cZa*E_omjnzT4dW`b2O4>$d+Yvoo|H zDwU~+xIS8%R%^Q6=nh|^+<UkO!6xRpoeqqQ3+Yu)p(hb+KEWumS9HAyC1(()E3_Fa z0rD_W`?O4jCb$dUjLR}g_awii|AMVdtfe+Pyn?y%ea|W~Vd=czZ=tbuK~le{Rb;V; z<Q|rNbUL&KXGL?dB-H^tp{=L0YD{wARDOL_*Vs^hedkN<Jb`NU2nkJEOZS+fuWOg8 zXSVh{4zo_Ep+X$lhD@j1^&uZMlq|MM_k}Hmj-h(c_QMTyf-^lx1kef95Ywhe{E5Yc x8pq<Je=JI_WT`EG&d8I~q7bYkp}WHT_zT0_`gUCW$T|Q3002ovPDHLkV1gJvE1LiS literal 0 HcmV?d00001 diff --git a/assets/images/desktop_transactions_solid_icon.png b/assets/images/desktop_transactions_solid_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d793803da134d885e089e6993a925df8f810d151 GIT binary patch literal 390 zcmV;10eSw3P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0UJp~K~#7F?UdmS zf-n$;PvZcN&=GV4HxM^q191Y}K%GE0&<%(azy`R1Fu{410}m)jBP9OdOI{?k_g-^H z0T}r!5K)m1q?q)??&!TC>8w54%xz5D!itco$nZ&X_#N%xnF1EjA8S*9g8nEvF%yd$ z7=S4Z%p`x%;|DtuQUuxT@|HyWyO<EA-DpQlnsc4vJKzd0uXY@?!(rpA4iaHok`E@( zjuq?u%=NzbzX+LCdYbq0M7mYZ;E=YwPwoXa+zX#Qt)(=EV^e@>;Ck&rT2f(>pPWfa zJNR6%d%YU*_`Nfw-n+c!idh6&U;?{+>>M_VD|=(E4;lkR$OW_4&J-4MfMV0aoa@^I kY!IzdV$DO-9T;ij1priDa{-xuivR!s07*qoM6N<$f_A~1=l}o! literal 0 HcmV?d00001 diff --git a/assets/images/settings_outline.png b/assets/images/settings_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..3230f00c45cdb4a258fe38fc67edfb5fe61c1fd9 GIT binary patch literal 871 zcmV-t1DO1YP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0{lrtK~#7F#Z_HS z+dvSWSyM_MDuUGaIwxp20p_g%RnQZ(oPd~s)Sm#MN!6<SIGliT0;no5Z-f)T4KVMO zNYxh<$vd4{C&n?RhDudOa=cm3-}lYz4!}Payo*w8{kwze!8{-p%m!fxApF*-PJhj_ zRe1`_oBnH5L&v;yNPI^TN^H7QTlbwj`yt2`nqi<pqx!r!82IQC6;*iIzE=Rrm|qhB z`o!vX+aX|y(++N$>NcZr-hDYJSr$5+#51u;(>6fSs(0k^Ylk04C<uo~&UtMU(01BA zY2-aBSe80v=WK_wV#CP1%m8)HL~E*`hP6huT(lGvLx4GXHy5Y&(*c@G<#u0(G)yvu zV?>e&Rg$&A1T~T^h;#T$se5~cP>-Cl*PyXbo{R(3J>L#BoHGLV<I+?s%XoS7kPE6N zOskUKy{$!;*I7@Y_mH$*Tb#b=QFPUjU!~O3ETh`Sq2Za>h_svqZqY5-%aERrA6ECL z`D+lkFFSqyRy==oFmpZ!h*BSyYcVbl>$yl+dN0`~Bx(|>h_4p=j01?Uq4~fCt2Z|e zVkCs{iU`k=)HSM8EhIWqO4>6L;a{4Bk+#HF3@Dia^2tfUGkZiGxJ?K~um6k`vQ*Tc z;2F_yf=H#P$dRmU9nBNd2B>#q1a3KC^lRcHc^0q_kK5}~;Bh~@=`2>bpiEPVkj^4h zyk?4`dJGCTM2l-S|4UtsNunwVPiuVv8S-JlIGSVE9V8V<k+Ymf!J}*CNjN+jBQTM- z{A2!TbL8;>itZ`t1ABehh0LFV<H!1uw5cmIKr3bIu4;dNTMn|Ez4_$ZYFIl;>!7*x zqFs{~*I<XCj?oPaoIZa-g<NF)eYtDo?n&v+vhE&SdkVO#Y-y5P+qCzAxu$*L*$S<3 z!@GjYjr5fy0)f?qqOl>F3wkc;{7%B{xhKQr=DvT<&wieL?nIM&pU>Z{DIWF)54UMV xp<c_gzthl<rX0>tZ|L(m=<ubtovzD&k>3b4f*%gWMKAyW002ovPDHLkV1jsFlE44} literal 0 HcmV?d00001 diff --git a/assets/images/support_icon.png b/assets/images/support_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6ec99fd1ef8fe39f50ce854500e89cc2c4fb26 GIT binary patch literal 819 zcmV-31I+x1P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0?0{3K~#7F#Z_HW z6G0H}p50LJAnXyA6BtiGc(Dk+$O*s`kS%^nEie<H)KYYp6G%>wSQf5ttS2y@z~u(` zWQ1he?oG0!l1)OiEV^nlliA+>rvJVM_|Jlq6rR61%Uc!}SoJ<@yU8KpC4l*QeTDVc z&x-M10qni~R6xrv13{sC6ca+6><l@GnD1JM+AZU~+<nxzH9*<_;u}My#HfL)Z8rn$ ztzGu(Xg_q@CIK(=<Dvq6;1w$q0rvgy0~pvKq7&Qttp^QfdmCy75TMh;t>TlB0DFE| zMgy9k%E3-i!xVA@g2Ca|iZ=wHIhk!seH&#w$%)Es0+K_9PL8%$>q9!np99X#CWm$T zXr~y-nd-f4*~;f7&N43GsO)pz7C7lH8>#V)3xj$RQn7(n5-vyqp1ulQOsd5UB`E<{ zxWUd|TI$!LrJ4_9&*4}fx(MfsjHycHNqMILIh!8KYAF?E0dU>djWHE5&}_~+eBcn7 zpINH5eHF;cUKW!7T|btHu%W$`IP0TsJflt?u*LwfZK)tjS1z&tK8%&(#)Pg1gy<m9 z#Bu~_Jl3-mENgFc2NUN(a+Ly1B?R#}<Ac%(k)aC?+R`LtB>HFwCW>Mz;iz1s09-*0 z?uhl0Uvq*noqM>o8cZ@S70H~HXR4_tVDi%@ie#N{&6LJ{y#FrzA<sCS+)RfYb2ipf z0AWH#jYz4)3-~t?29A1pveV->R2c63o|%&_^s>HWB3m41S-6ael3CWQ_0G~Nl3EuA z<uWO~5XC8_pD6oh!w_BX9fzmL2_MDpgHqBpXJKKkEc1g1sz=+!jsEwU1E-JQW8tUk zD-{inMln3UFe}U{23_%<8Fr{hMvhGFpXFPmeC&=)LTVsjuY}!Rr}C{M-dB3`d~1Lk xAQ_E9hH8mHk_uzcm`#7T+IFOOA2#4F<qv7zM%aN@^0WW|002ovPDHLkV1kAiZ!Q1; literal 0 HcmV?d00001 diff --git a/assets/images/wallet_outline.png b/assets/images/wallet_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..e75131b05df6207aaddbfb3560b54250654d26f2 GIT binary patch literal 628 zcmV-)0*n2LP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0trb(K~#7F?Nv=q z+fWdmc?NaCqFz>tTO6s7sGIr-?yd@MDJQ5oL66XK18$JqfTEJ9J931usS<U8C9uSf zw}3?!e!~nr2b-V#7?yld;&?onZ~opFfEUaF7H?cXbXQ)j#ER5MLUP44M+}XCi80wx z=9vE3@Ne=ULG42;z)T(5pc)7geyV4NC-z71<#(27EB)UalnCB5Kl{tHd}zP`B5t1! z5BjhmBtgh<k4fB%@kL!H{p;kEd^n&&0`kGoR+Zp_^j~iv$w?*B=G=ZwfTRMx=wfMA zV3X4_!`!xHud7@^QgnXT`1&K`n$uBtcUsU|vlVlKn{c}jF1I?6w#&d(ZsgvT2Fgy~ z*tR(}`1(ehgSR=kpkVHEtNbN$c8e7(jGN3c<~7+#huvSrXI>-XZN%44BH+xwbLImH z{<iVuSZpVwKlQtQ3aVq$Qe$5ap>hR;W?%a6tw%pkM+f!C{3;Wq!(OM>+z5FQq($+a zEewe0w|vQ4q}idQJAA({@98Anm@C<qS(JhU=}cui6AQ0fhbMmy&j5+ztwocwb0Za~ zBm>uLMj<=_wY8R<2baJMN(5@`bU<X6ux*_dhW9=Sm2&oE79!iZy(HIG4SgZwAtihw zXUaAExpH#B$~-ydlL+5E0)gm<D618}Rhah=hfo_dN0;NvPA=0gm>X9~tK37+zUB`A O0000<MNUMnLSTXv4Hsho literal 0 HcmV?d00001 diff --git a/assets/images/wallet_solid.png b/assets/images/wallet_solid.png new file mode 100644 index 0000000000000000000000000000000000000000..0997221f2ba544e232ce295c39c642be8c8bac1e GIT binary patch literal 581 zcmV-L0=oT)P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0oqAKK~#7F?NmEX z!$1(-SqEAS6(~V60+EzCLelXGLY#nb0>TM^696XwPC$_OmD~W+AU*|K5J9;T28Dok zm`&ouaqI*+lzENU<DGds^WK`VA^HG<fn;W>2*_8FVHOye6f*nNkccSgX}jFreY?LB zkp#*4<wF1{viOjwRKD_bVS542{3jo*xQ5BOapDgoyakPAoy7o+QqU+*X(5~M%(QZ* z6xXtYfIe8zElDp(7u}85$IEiBP3;aCSb>vmaRh<DPQnt=Q2E!pOFK1R*havjKcCh@ zDR)h4!C3@I4;xjZ?XKd8N0qw^dpf<cN7Z&h4rc*}9leeu=mknXPR$i^RH)PpNv%a- z*H~-<r|Q_;fNZ-KTdO>r&uF6H-UN$*ahMpZK~<9T>hCnN)v4LV&`TMsgIc1RAE}ON z_tZ~J8kCHL5o(551mu8CA<9xaseAS<+&0LYNT!#c18yjj9W2_B4a=@O>G6DHGP7VA zjORe{Rxa6Ak|z5d1mx$NsA7?gH2l8%@%>r$Zf^G;k04#pjGOEq;Cwmb9$`y^ANbJ( zk`0ane>W`x{U7fm+MkNQJe-yMfzZEODx+_yvi!nD0l5-fars_zuA_7qq9=X<QRB%I TPSrx>00000NkvXXu0mjfLBjiN literal 0 HcmV?d00001 diff --git a/cw_monero/.gitignore b/cw_monero/.gitignore index c8bb78494..ebb19df82 100644 --- a/cw_monero/.gitignore +++ b/cw_monero/.gitignore @@ -8,4 +8,7 @@ build/ ios/External/ android/.externalNativeBuild/ -android/.cxx/ \ No newline at end of file +android/.cxx/ + +macos/cw_monero.podspec +macos/External/ \ No newline at end of file diff --git a/cw_monero/.metadata b/cw_monero/.metadata index 36ba765ff..46a2f7f6f 100644 --- a/cw_monero/.metadata +++ b/cw_monero/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 798e4272a2e43d7daab75f225a13442e384ee0cd - channel: dev + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + channel: stable project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: macos + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_monero/analysis_options.yaml b/cw_monero/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_monero/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_monero/example/.gitignore b/cw_monero/example/.gitignore new file mode 100644 index 000000000..24476c5d1 --- /dev/null +++ b/cw_monero/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/cw_monero/example/README.md b/cw_monero/example/README.md new file mode 100644 index 000000000..18cf6d109 --- /dev/null +++ b/cw_monero/example/README.md @@ -0,0 +1,16 @@ +# cw_monero_example + +Demonstrates how to use the cw_monero plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/cw_monero/example/analysis_options.yaml b/cw_monero/example/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/cw_monero/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_monero/example/lib/main.dart b/cw_monero/example/lib/main.dart new file mode 100644 index 000000000..e4374f097 --- /dev/null +++ b/cw_monero/example/lib/main.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:cw_monero/cw_monero.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State<MyApp> createState() => _MyAppState(); +} + +class _MyAppState extends State<MyApp> { + String _platformVersion = 'Unknown'; + final _cwMoneroPlugin = CwMonero(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future<void> initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _cwMoneroPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/cw_monero/example/macos/.gitignore b/cw_monero/example/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/cw_monero/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig b/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig b/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift b/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..e25d64097 --- /dev/null +++ b/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import cw_monero +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/cw_monero/example/macos/Podfile b/cw_monero/example/macos/Podfile new file mode 100644 index 000000000..dade8dfad --- /dev/null +++ b/cw_monero/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/cw_monero/example/macos/Podfile.lock b/cw_monero/example/macos/Podfile.lock new file mode 100644 index 000000000..692176b30 --- /dev/null +++ b/cw_monero/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + +PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c + +COCOAPODS: 1.11.2 diff --git a/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj b/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..472859e8c --- /dev/null +++ b/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 428E7496E2068D0AB138F295 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; + 33CC10ED2044A3C60003C045 /* cw_monero_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cw_monero_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; + A9CDA1605413332AB9056C23 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; + C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E434913D71DC2682EF8E9059 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; + EEF09839C86335F78056F812 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 428E7496E2068D0AB138F295 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = "<group>"; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 77870A4C94A9AB6EEC2EE261 /* Pods */, + ); + sourceTree = "<group>"; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* cw_monero_example.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = "<group>"; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = "<group>"; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = "<group>"; + }; + 77870A4C94A9AB6EEC2EE261 /* Pods */ = { + isa = PBXGroup; + children = ( + EEF09839C86335F78056F812 /* Pods-Runner.debug.xcconfig */, + A9CDA1605413332AB9056C23 /* Pods-Runner.release.xcconfig */, + E434913D71DC2682EF8E9059 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = "<group>"; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0A239C1738C005E3F6E4DFC6 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 0CEAA82AE8A029C31B39F234 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* cw_monero_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0A239C1738C005E3F6E4DFC6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 0CEAA82AE8A029C31B39F234 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..4e44b7ced --- /dev/null +++ b/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1300" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "cw_monero_example.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "cw_monero_example.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "cw_monero_example.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Profile" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "cw_monero_example.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "group:Runner.xcodeproj"> + </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> +</Workspace> diff --git a/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/cw_monero/example/macos/Runner/AppDelegate.swift b/cw_monero/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/cw_monero/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K<V}W|Nr-^PFT-<B<I8<k2moS-$dQqjwO2}38)2pwoy<M({V6lSaDit{<(Sw zPXRaPYYNUY7O1^kFoWZ1&9ATX7d}2E-*#lOy=2(EzuoIt*085BSnql>{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2<IW)xrk zTF<k-{!d~7Q&wNdv9*#ZTy2c$asSgRzRIqUe32s`sn+(h{GXA{&u{&Xv->+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-u<zsp4Sq6Wua z5BBhjsWTfpZEpHBEIjPnnRxiIKToJ|*&=PntIR&BpDzYU*}YSmk@;dK=M)=9O`bU( o30)I1CTw2MWPDjr#qiV&hQFoXIy#f5hk;_j)78&qol`;+052KE`v3p{ literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bdb57226d5f2bd20f11934f4903f16459cf52379 GIT binary patch literal 14142 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 K%C(A?AO9DJ-0fok literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..f083318e09ca1b6b8484b1694a149f0c5d5cac1d GIT binary patch literal 1066 zcmV+_1l9YAP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qc<j#1d-MCvdv9j8z=r(i2q<p&rfD9eu@2G*X@FEG z6~cb9uStEZLK5`yo%9{)UDA7sqGXR#mxB;+fV7|VZ_5LkFz5hj?;@j8fQ!XEq!(M{ zYz-RZu=m?3<!>b^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZx<jK$)1#?a4VGrm!=3lJYK6-%z3AD1isd9rK<& zotoW{3D^d+4f&nH7`%5jZrktykBXBK9scZ-LUvwn+Zf6O*3BH=2BOx=IRz|CDC|>l zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9<VTK~ebn>NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6<BmbAI7C`%X>(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbG<m)%ac-@w;CT4TyWxt kNncSvOvqtF{%Hh$1H=*W-r{pWaR2}S07*qoM6N<$g8Wt7Y5)KL literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..326c0e72c9d820600887813b3b98d0dd69c5d4e8 GIT binary patch literal 36406 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 literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 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 literal 0 HcmV?d00001 diff --git a/cw_monero/example/macos/Runner/Base.lproj/MainMenu.xib b/cw_monero/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/cw_monero/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target"> + <connections> + <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/> + <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> + </connections> + </customObject> + <customObject id="YLy-65-1bz" customClass="NSFontManager"/> + <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="APP_NAME" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About APP_NAME" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="-1" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="-1" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="-1" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="-1" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="-1" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="-1" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="-1" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="EPT-qC-fAb"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/> + </menuItem> + </items> + <point key="canvasLocation" x="142" y="-258"/> + </menu> + <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> + <rect key="contentRect" x="335" y="390" width="800" height="600"/> + <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/> + <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> + <rect key="frame" x="0.0" y="0.0" width="800" height="600"/> + <autoresizingMask key="autoresizingMask"/> + </view> + </window> + </objects> +</document> diff --git a/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig b/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..a80a25602 --- /dev/null +++ b/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = cw_monero_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cwMoneroExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.cakewallet. All rights reserved. diff --git a/cw_monero/example/macos/Runner/Configs/Debug.xcconfig b/cw_monero/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/cw_monero/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/cw_monero/example/macos/Runner/Configs/Release.xcconfig b/cw_monero/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/cw_monero/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig b/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/cw_monero/example/macos/Runner/DebugProfile.entitlements b/cw_monero/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/cw_monero/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>com.apple.security.cs.allow-jit</key> + <true/> + <key>com.apple.security.network.server</key> + <true/> +</dict> +</plist> diff --git a/cw_monero/example/macos/Runner/Info.plist b/cw_monero/example/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/cw_monero/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>$(FLUTTER_BUILD_NAME)</string> + <key>CFBundleVersion</key> + <string>$(FLUTTER_BUILD_NUMBER)</string> + <key>LSMinimumSystemVersion</key> + <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>NSHumanReadableCopyright</key> + <string>$(PRODUCT_COPYRIGHT)</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/cw_monero/example/macos/Runner/MainFlutterWindow.swift b/cw_monero/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/cw_monero/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/cw_monero/example/macos/Runner/Release.entitlements b/cw_monero/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/cw_monero/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> +</dict> +</plist> diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock new file mode 100644 index 000000000..772ff47bd --- /dev/null +++ b/cw_monero/example/pubspec.lock @@ -0,0 +1,403 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + async: + dependency: transitive + description: + name: async + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + cw_core: + dependency: transitive + description: + path: "../../cw_core" + relative: true + source: path + version: "0.0.1" + cw_monero: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + url: "https://pub.dev" + source: hosted + version: "2.0.6+5" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" + source: hosted + version: "0.13.5" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" + source: hosted + version: "0.12.13" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + mobx: + dependency: transitive + description: + name: mobx + sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + url: "https://pub.dev" + source: hosted + version: "2.1.3+1" + path: + dependency: transitive + description: + name: path + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" + source: hosted + version: "1.8.2" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" + source: hosted + version: "2.0.12" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" + source: hosted + version: "2.0.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" + source: hosted + version: "2.1.7" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905 + url: "https://pub.dev" + source: hosted + version: "2.0.7" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" + source: hosted + version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" + source: hosted + version: "3.6.2" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" + source: hosted + version: "0.4.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef + url: "https://pub.dev" + source: hosted + version: "2.6.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" + source: hosted + version: "0.2.0+3" +sdks: + dart: ">=2.18.1 <3.0.0" + flutter: ">=3.0.0" diff --git a/cw_monero/example/pubspec.yaml b/cw_monero/example/pubspec.yaml new file mode 100644 index 000000000..2dee5337f --- /dev/null +++ b/cw_monero/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: cw_monero_example +description: Demonstrates how to use the cw_monero plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.18.1 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + cw_monero: + # When depending on this package from a real application you should use: + # cw_monero: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/example/test/widget_test.dart b/cw_monero/example/test/widget_test.dart new file mode 100644 index 000000000..b37e6313d --- /dev/null +++ b/cw_monero/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_monero_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/cw_monero/lib/cw_monero.dart b/cw_monero/lib/cw_monero.dart new file mode 100644 index 000000000..7945a020e --- /dev/null +++ b/cw_monero/lib/cw_monero.dart @@ -0,0 +1,8 @@ + +import 'cw_monero_platform_interface.dart'; + +class CwMonero { + Future<String?> getPlatformVersion() { + return CwMoneroPlatform.instance.getPlatformVersion(); + } +} diff --git a/cw_monero/lib/cw_monero_method_channel.dart b/cw_monero/lib/cw_monero_method_channel.dart new file mode 100644 index 000000000..1cbca9f2c --- /dev/null +++ b/cw_monero/lib/cw_monero_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_monero_platform_interface.dart'; + +/// An implementation of [CwMoneroPlatform] that uses method channels. +class MethodChannelCwMonero extends CwMoneroPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_monero'); + + @override + Future<String?> getPlatformVersion() async { + final version = await methodChannel.invokeMethod<String>('getPlatformVersion'); + return version; + } +} diff --git a/cw_monero/lib/cw_monero_platform_interface.dart b/cw_monero/lib/cw_monero_platform_interface.dart new file mode 100644 index 000000000..6c9b20a25 --- /dev/null +++ b/cw_monero/lib/cw_monero_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_monero_method_channel.dart'; + +abstract class CwMoneroPlatform extends PlatformInterface { + /// Constructs a CwMoneroPlatform. + CwMoneroPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwMoneroPlatform _instance = MethodChannelCwMonero(); + + /// The default instance of [CwMoneroPlatform] to use. + /// + /// Defaults to [MethodChannelCwMonero]. + static CwMoneroPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwMoneroPlatform] when + /// they register themselves. + static set instance(CwMoneroPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future<String?> getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/cw_monero/macos/Classes/CwMoneroPlugin.swift b/cw_monero/macos/Classes/CwMoneroPlugin.swift new file mode 100644 index 000000000..d4ff81e1c --- /dev/null +++ b/cw_monero/macos/Classes/CwMoneroPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwMoneroPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_monero", binaryMessenger: registrar.messenger) + let instance = CwMoneroPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_monero/macos/Classes/CwWalletListener.h b/cw_monero/macos/Classes/CwWalletListener.h new file mode 100644 index 000000000..cbfcb0c4e --- /dev/null +++ b/cw_monero/macos/Classes/CwWalletListener.h @@ -0,0 +1,23 @@ +#include <stdint.h> + +struct CWMoneroWalletListener; + +typedef int8_t (*on_new_block_callback)(uint64_t height); +typedef int8_t (*on_need_to_refresh_callback)(); + +typedef struct CWMoneroWalletListener +{ + // on_money_spent_callback *on_money_spent; + // on_money_received_callback *on_money_received; + // on_unconfirmed_money_received_callback *on_unconfirmed_money_received; + // on_new_block_callback *on_new_block; + // on_updated_callback *on_updated; + // on_refreshed_callback *on_refreshed; + + on_new_block_callback on_new_block; +} CWMoneroWalletListener; + +struct TestListener { + // int8_t x; + on_new_block_callback on_new_block; +}; \ No newline at end of file diff --git a/cw_monero/macos/Classes/monero_api.cpp b/cw_monero/macos/Classes/monero_api.cpp new file mode 100644 index 000000000..56548e79e --- /dev/null +++ b/cw_monero/macos/Classes/monero_api.cpp @@ -0,0 +1,798 @@ +#include <stdint.h> +#include "cstdlib" +#include <chrono> +#include <functional> +#include <iostream> +#include <unistd.h> +#include <mutex> +#include "thread" +#include "CwWalletListener.h" +#if __APPLE__ +// Fix for randomx on ios +void __clear_cache(void* start, void* end) { } +#include "../External/macos/include/wallet2_api.h" +#else +#include "../External/android/include/wallet2_api.h" +#endif + +using namespace std::chrono_literals; +#ifdef __cplusplus +extern "C" +{ +#endif + const uint64_t MONERO_BLOCK_SIZE = 1000; + + struct Utf8Box + { + char *value; + + Utf8Box(char *_value) + { + value = _value; + } + }; + + struct SubaddressRow + { + uint64_t id; + char *address; + char *label; + + SubaddressRow(std::size_t _id, char *_address, char *_label) + { + id = static_cast<uint64_t>(_id); + address = _address; + label = _label; + } + }; + + struct AccountRow + { + uint64_t id; + char *label; + + AccountRow(std::size_t _id, char *_label) + { + id = static_cast<uint64_t>(_id); + label = _label; + } + }; + + struct MoneroWalletListener : Monero::WalletListener + { + uint64_t m_height; + bool m_need_to_refresh; + bool m_new_transaction; + + MoneroWalletListener() + { + m_height = 0; + m_need_to_refresh = false; + m_new_transaction = false; + } + + void moneySpent(const std::string &txId, uint64_t amount) + { + m_new_transaction = true; + } + + void moneyReceived(const std::string &txId, uint64_t amount) + { + m_new_transaction = true; + } + + void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) + { + m_new_transaction = true; + } + + void newBlock(uint64_t height) + { + m_height = height; + } + + void updated() + { + m_new_transaction = true; + } + + void refreshed() + { + m_need_to_refresh = true; + } + + void resetNeedToRefresh() + { + m_need_to_refresh = false; + } + + bool isNeedToRefresh() + { + return m_need_to_refresh; + } + + bool isNewTransactionExist() + { + return m_new_transaction; + } + + void resetIsNewTransactionExist() + { + m_new_transaction = false; + } + + uint64_t height() + { + return m_height; + } + }; + + struct TransactionInfoRow + { + uint64_t amount; + uint64_t fee; + uint64_t blockHeight; + uint64_t confirmations; + uint32_t subaddrAccount; + int8_t direction; + int8_t isPending; + uint32_t subaddrIndex; + + char *hash; + char *paymentId; + + int64_t datetime; + + TransactionInfoRow(Monero::TransactionInfo *transaction) + { + amount = transaction->amount(); + fee = transaction->fee(); + blockHeight = transaction->blockHeight(); + subaddrAccount = transaction->subaddrAccount(); + std::set<uint32_t>::iterator it = transaction->subaddrIndex().begin(); + subaddrIndex = *it; + confirmations = transaction->confirmations(); + datetime = static_cast<int64_t>(transaction->timestamp()); + direction = transaction->direction(); + isPending = static_cast<int8_t>(transaction->isPending()); + std::string *hash_str = new std::string(transaction->hash()); + hash = strdup(hash_str->c_str()); + paymentId = strdup(transaction->paymentId().c_str()); + } + }; + + struct PendingTransactionRaw + { + uint64_t amount; + uint64_t fee; + char *hash; + char *hex; + char *txKey; + Monero::PendingTransaction *transaction; + + PendingTransactionRaw(Monero::PendingTransaction *_transaction) + { + transaction = _transaction; + amount = _transaction->amount(); + fee = _transaction->fee(); + hash = strdup(_transaction->txid()[0].c_str()); + hex = strdup(_transaction->hex()[0].c_str()); + txKey = strdup(_transaction->txKey()[0].c_str()); + } + }; + + Monero::Wallet *m_wallet; + Monero::TransactionHistory *m_transaction_history; + MoneroWalletListener *m_listener; + Monero::Subaddress *m_subaddress; + Monero::SubaddressAccount *m_account; + uint64_t m_last_known_wallet_height; + uint64_t m_cached_syncing_blockchain_height = 0; + std::mutex store_lock; + bool is_storing = false; + + void change_current_wallet(Monero::Wallet *wallet) + { + m_wallet = wallet; + m_listener = nullptr; + + + if (wallet != nullptr) + { + m_transaction_history = wallet->history(); + } + else + { + m_transaction_history = nullptr; + } + + if (wallet != nullptr) + { + m_account = wallet->subaddressAccount(); + } + else + { + m_account = nullptr; + } + + if (wallet != nullptr) + { + m_subaddress = wallet->subaddress(); + } + else + { + m_subaddress = nullptr; + } + } + + Monero::Wallet *get_current_wallet() + { + return m_wallet; + } + + bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) + { + Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType); + Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); + Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (wallet->status() != Monero::Wallet::Status_Ok) + { + error = strdup(wallet->errorString().c_str()); + return false; + } + + change_current_wallet(wallet); + + return true; + } + + bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( + std::string(path), + std::string(password), + std::string(seed), + _networkType, + (uint64_t)restoreHeight); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + + bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( + std::string(path), + std::string(password), + std::string(language), + _networkType, + (uint64_t)restoreHeight, + std::string(address), + std::string(viewKey), + std::string(spendKey)); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + + bool load_wallet(char *path, char *password, int32_t nettype) + { + nice(19); + Monero::NetworkType networkType = static_cast<Monero::NetworkType>(nettype); + Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); + Monero::Wallet *wallet = walletManager->openWallet(std::string(path), std::string(password), networkType); + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + change_current_wallet(wallet); + + return !(status != Monero::Wallet::Status_Ok || !errorString.empty()); + } + + char *error_string() { + return strdup(get_current_wallet()->errorString().c_str()); + } + + + bool is_wallet_exist(char *path) + { + return Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(path)); + } + + void close_current_wallet() + { + Monero::WalletManagerFactory::getWalletManager()->closeWallet(get_current_wallet()); + change_current_wallet(nullptr); + } + + char *get_filename() + { + return strdup(get_current_wallet()->filename().c_str()); + } + + char *secret_view_key() + { + return strdup(get_current_wallet()->secretViewKey().c_str()); + } + + char *public_view_key() + { + return strdup(get_current_wallet()->publicViewKey().c_str()); + } + + char *secret_spend_key() + { + return strdup(get_current_wallet()->secretSpendKey().c_str()); + } + + char *public_spend_key() + { + return strdup(get_current_wallet()->publicSpendKey().c_str()); + } + + char *get_address(uint32_t account_index, uint32_t address_index) + { + return strdup(get_current_wallet()->address(account_index, address_index).c_str()); + } + + + const char *seed() + { + return strdup(get_current_wallet()->seed().c_str()); + } + + uint64_t get_full_balance(uint32_t account_index) + { + return get_current_wallet()->balance(account_index); + } + + uint64_t get_unlocked_balance(uint32_t account_index) + { + return get_current_wallet()->unlockedBalance(account_index); + } + + uint64_t get_current_height() + { + return get_current_wallet()->blockChainHeight(); + } + + uint64_t get_node_height() + { + return get_current_wallet()->daemonBlockChainHeight(); + } + + bool connect_to_node(char *error) + { + nice(19); + bool is_connected = get_current_wallet()->connectToDaemon(); + + if (!is_connected) + { + error = strdup(get_current_wallet()->errorString().c_str()); + } + + return is_connected; + } + + bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + { + nice(19); + Monero::Wallet *wallet = get_current_wallet(); + + std::string _login = ""; + std::string _password = ""; + + if (login != nullptr) + { + _login = std::string(login); + } + + if (password != nullptr) + { + _password = std::string(password); + } + + bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + + if (!inited) + { + error = strdup(wallet->errorString().c_str()); + } else if (!wallet->connectToDaemon()) { + error = strdup(wallet->errorString().c_str()); + } + + return inited; + } + + bool is_connected() + { + return get_current_wallet()->connected(); + } + + void start_refresh() + { + get_current_wallet()->refreshAsync(); + get_current_wallet()->startRefresh(); + } + + void set_refresh_from_block_height(uint64_t height) + { + get_current_wallet()->setRefreshFromBlockHeight(height); + } + + void set_recovering_from_seed(bool is_recovery) + { + get_current_wallet()->setRecoveringFromSeed(is_recovery); + } + + void store(char *path) + { + store_lock.lock(); + if (is_storing) { + return; + } + + is_storing = true; + get_current_wallet()->store(std::string(path)); + is_storing = false; + store_lock.unlock(); + } + + bool set_password(char *password, Utf8Box &error) { + bool is_changed = get_current_wallet()->setPassword(std::string(password)); + + if (!is_changed) { + error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); + } + + return is_changed; + } + + bool transaction_create(char *address, char *payment_id, char *amount, + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + { + nice(19); + + auto priority = static_cast<Monero::PendingTransaction::Priority>(priority_raw); + std::string _payment_id; + Monero::PendingTransaction *transaction; + + if (payment_id != nullptr) + { + _payment_id = std::string(payment_id); + } + + if (amount != nullptr) + { + uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + } + else + { + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional<uint64_t>(), m_wallet->defaultMixin(), priority, subaddr_account); + } + + int status = transaction->status(); + + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) + { + error = Utf8Box(strdup(transaction->errorString().c_str())); + return false; + } + + if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + pendingTransaction = PendingTransactionRaw(transaction); + return true; + } + + bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + { + nice(19); + + std::vector<std::string> _addresses; + std::vector<uint64_t> _amounts; + + for (int i = 0; i < size; i++) { + _addresses.push_back(std::string(*addresses)); + _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); + addresses++; + amounts++; + } + + auto priority = static_cast<Monero::PendingTransaction::Priority>(priority_raw); + std::string _payment_id; + Monero::PendingTransaction *transaction; + + if (payment_id != nullptr) + { + _payment_id = std::string(payment_id); + } + + transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, m_wallet->defaultMixin(), priority, subaddr_account); + + int status = transaction->status(); + + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) + { + error = Utf8Box(strdup(transaction->errorString().c_str())); + return false; + } + + if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + pendingTransaction = PendingTransactionRaw(transaction); + return true; + } + + bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) + { + bool committed = transaction->transaction->commit(); + + if (!committed) + { + error = Utf8Box(strdup(transaction->transaction->errorString().c_str())); + } else if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + return committed; + } + + uint64_t get_node_height_or_update(uint64_t base_eight) + { + if (m_cached_syncing_blockchain_height < base_eight) { + m_cached_syncing_blockchain_height = base_eight; + } + + return m_cached_syncing_blockchain_height; + } + + uint64_t get_syncing_height() + { + if (m_listener == nullptr) { + return 0; + } + + uint64_t height = m_listener->height(); + + if (height <= 1) { + return 0; + } + + if (height != m_last_known_wallet_height) + { + m_last_known_wallet_height = height; + } + + return height; + } + + uint64_t is_needed_to_refresh() + { + if (m_listener == nullptr) { + return false; + } + + bool should_refresh = m_listener->isNeedToRefresh(); + + if (should_refresh) { + m_listener->resetNeedToRefresh(); + } + + return should_refresh; + } + + uint8_t is_new_transaction_exist() + { + if (m_listener == nullptr) { + return false; + } + + bool is_new_transaction_exist = m_listener->isNewTransactionExist(); + + if (is_new_transaction_exist) + { + m_listener->resetIsNewTransactionExist(); + } + + return is_new_transaction_exist; + } + + void set_listener() + { + m_last_known_wallet_height = 0; + + if (m_listener != nullptr) + { + free(m_listener); + } + + m_listener = new MoneroWalletListener(); + get_current_wallet()->setListener(m_listener); + } + + int64_t *subaddrress_get_all() + { + std::vector<Monero::SubaddressRow *> _subaddresses = m_subaddress->getAll(); + size_t size = _subaddresses.size(); + int64_t *subaddresses = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::SubaddressRow *row = _subaddresses[i]; + SubaddressRow *_row = new SubaddressRow(row->getRowId(), strdup(row->getAddress().c_str()), strdup(row->getLabel().c_str())); + subaddresses[i] = reinterpret_cast<int64_t>(_row); + } + + return subaddresses; + } + + int32_t subaddrress_size() + { + std::vector<Monero::SubaddressRow *> _subaddresses = m_subaddress->getAll(); + return _subaddresses.size(); + } + + void subaddress_add_row(uint32_t accountIndex, char *label) + { + m_subaddress->addRow(accountIndex, std::string(label)); + } + + void subaddress_set_label(uint32_t accountIndex, uint32_t addressIndex, char *label) + { + m_subaddress->setLabel(accountIndex, addressIndex, std::string(label)); + } + + void subaddress_refresh(uint32_t accountIndex) + { + m_subaddress->refresh(accountIndex); + } + + int32_t account_size() + { + std::vector<Monero::SubaddressAccountRow *> _accocunts = m_account->getAll(); + return _accocunts.size(); + } + + int64_t *account_get_all() + { + std::vector<Monero::SubaddressAccountRow *> _accocunts = m_account->getAll(); + size_t size = _accocunts.size(); + int64_t *accocunts = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::SubaddressAccountRow *row = _accocunts[i]; + AccountRow *_row = new AccountRow(row->getRowId(), strdup(row->getLabel().c_str())); + accocunts[i] = reinterpret_cast<int64_t>(_row); + } + + return accocunts; + } + + void account_add_row(char *label) + { + m_account->addRow(std::string(label)); + } + + void account_set_label_row(uint32_t account_index, char *label) + { + m_account->setLabel(account_index, label); + } + + void account_refresh() + { + m_account->refresh(); + } + + int64_t *transactions_get_all() + { + std::vector<Monero::TransactionInfo *> transactions = m_transaction_history->getAll(); + size_t size = transactions.size(); + int64_t *transactionAddresses = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::TransactionInfo *row = transactions[i]; + TransactionInfoRow *tx = new TransactionInfoRow(row); + transactionAddresses[i] = reinterpret_cast<int64_t>(tx); + } + + return transactionAddresses; + } + + void transactions_refresh() + { + m_transaction_history->refresh(); + } + + int64_t transactions_count() + { + return m_transaction_history->count(); + } + + int LedgerExchange( + unsigned char *command, + unsigned int cmd_len, + unsigned char *response, + unsigned int max_resp_len) + { + return -1; + } + + int LedgerFind(char *buffer, size_t len) + { + return -1; + } + + void on_startup() + { + Monero::Utils::onStartup(); + Monero::WalletManagerFactory::setLogLevel(0); + } + + void rescan_blockchain() + { + m_wallet->rescanBlockchainAsync(); + } + + char * get_tx_key(char * txId) + { + return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); + } + + char *get_subaddress_label(uint32_t accountIndex, uint32_t addressIndex) + { + return strdup(get_current_wallet()->getSubaddressLabel(accountIndex, addressIndex).c_str()); + } + + void set_trusted_daemon(bool arg) + { + m_wallet->setTrustedDaemon(arg); + } + + bool trusted_daemon() + { + return m_wallet->trustedDaemon(); + } + +#ifdef __cplusplus +} +#endif diff --git a/cw_monero/macos/Classes/monero_api.h b/cw_monero/macos/Classes/monero_api.h new file mode 100644 index 000000000..74258ba4c --- /dev/null +++ b/cw_monero/macos/Classes/monero_api.h @@ -0,0 +1,38 @@ +#include <stdint.h> +#include <stdio.h> +#include <stdbool.h> +#include "CwWalletListener.h" + +#ifdef __cplusplus +extern "C" { +#endif + +bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error); +bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error); +bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error); +void load_wallet(char *path, char *password, int32_t nettype); +bool is_wallet_exist(char *path); + +char *get_filename(); +const char *seed(); +char *get_address(uint32_t account_index, uint32_t address_index); +uint64_t get_full_balance(uint32_t account_index); +uint64_t get_unlocked_balance(uint32_t account_index); +uint64_t get_current_height(); +uint64_t get_node_height(); + +bool is_connected(); + +bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error); +bool connect_to_node(char *error); +void start_refresh(); +void set_refresh_from_block_height(uint64_t height); +void set_recovering_from_seed(bool is_recovery); +void store(char *path); + +void set_trusted_daemon(bool arg); +bool trusted_daemon(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/cw_monero/macos/cw_monero_base.podspec b/cw_monero/macos/cw_monero_base.podspec new file mode 100644 index 000000000..aac972c0f --- /dev/null +++ b/cw_monero/macos/cw_monero_base.podspec @@ -0,0 +1,56 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_monero.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_monero' + s.version = '0.0.1' + s.summary = 'CW Monero' + s.description = 'Cake Wallet wrapper over Monero project.' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'CakeWallet' => 'support@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/macos/libs/monero/include/External/ios/**/*.h' + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => '#___VALID_ARCHS___#', 'ENABLE_BITCODE' => 'NO' } + s.swift_version = '5.0' + s.libraries = 'iconv' + + s.subspec 'OpenSSL' do |openssl| + openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' + openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' + openssl.libraries = 'ssl', 'crypto' + openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } + end + + s.subspec 'Sodium' do |sodium| + sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' + sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libsodium.a' + sodium.libraries = 'sodium' + sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } + end + + s.subspec 'Unbound' do |unbound| + unbound.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' + unbound.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libunbound.a' + unbound.libraries = 'unbound' + unbound.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } + end + + s.subspec 'Boost' do |boost| + boost.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h', + boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libboost.a', + boost.libraries = 'boost' + boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } + end + + s.subspec 'Monero' do |monero| + monero.preserve_paths = 'External/macos/include/**/*.h' + monero.vendored_libraries = 'External/macos/lib/libmonero.a' + monero.libraries = 'monero' + monero.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include" } + end +end diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 6d5041dfa..066a0d4c3 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -40,8 +40,15 @@ flutter: # be modified. They are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: - androidPackage: com.cakewallet.monero - pluginClass: CwMoneroPlugin + platforms: + android: + package: com.cakewallet.monero + pluginClass: CwMoneroPlugin + ios: + pluginClass: CwMoneroPlugin + + macos: + pluginClass: CwMoneroPlugin # To add assets to your plugin package, add an assets section, like this: # assets: diff --git a/cw_monero/test/cw_monero_method_channel_test.dart b/cw_monero/test/cw_monero_method_channel_test.dart new file mode 100644 index 000000000..8c1f329f0 --- /dev/null +++ b/cw_monero/test/cw_monero_method_channel_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:cw_monero/cw_monero_method_channel.dart'; + +void main() { + MethodChannelCwMonero platform = MethodChannelCwMonero(); + const MethodChannel channel = MethodChannel('cw_monero'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/cw_monero/test/cw_monero_test.dart b/cw_monero/test/cw_monero_test.dart new file mode 100644 index 000000000..1eb8d6f79 --- /dev/null +++ b/cw_monero/test/cw_monero_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:cw_monero/cw_monero.dart'; +import 'package:cw_monero/cw_monero_platform_interface.dart'; +import 'package:cw_monero/cw_monero_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockCwMoneroPlatform + with MockPlatformInterfaceMixin + implements CwMoneroPlatform { + + @override + Future<String?> getPlatformVersion() => Future.value('42'); +} + +void main() { + final CwMoneroPlatform initialPlatform = CwMoneroPlatform.instance; + + test('$MethodChannelCwMonero is the default instance', () { + expect(initialPlatform, isInstanceOf<MethodChannelCwMonero>()); + }); + + test('getPlatformVersion', () async { + CwMonero cwMoneroPlugin = CwMonero(); + MockCwMoneroPlatform fakePlatform = MockCwMoneroPlatform(); + CwMoneroPlatform.instance = fakePlatform; + + expect(await cwMoneroPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/ios/CakeWallet/wakeLock.swift b/ios/CakeWallet/wakeLock.swift deleted file mode 100644 index 35f23eafa..000000000 --- a/ios/CakeWallet/wakeLock.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// wakeLock.swift -// Runner -// -// Created by Godwin Asuquo on 1/21/22. -// - -import Foundation -import UIKit - -func enableWakeScreen() -> Bool{ - UIApplication.shared.isIdleTimerDisabled = true - - return true -} - -func disableWakeScreen() -> Bool{ - UIApplication.shared.isIdleTimerDisabled = false - return true -} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5148714e5..50b9da031 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 20ED0868E1BD7E12278C0CB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B26E3F56D69167FBB1DC160A /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5AFFEBFD279AD49C00F906A4 /* wakeLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -282,7 +281,6 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - 5AFFEBFD279AD49C00F906A4 /* wakeLock.swift in Sources */, 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */, 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */, ); diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index e3f3da418..6d5f51aa3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -96,11 +96,6 @@ import UnstoppableDomainsResolution result(address) } - case "enableWakeScreen": - result(enableWakeScreen()) - - case "disableWakeScreen": - result(disableWakeScreen()) default: result(FlutterMethodNotImplemented) diff --git a/lib/buy/moonpay/moonpay_buy_provider.dart b/lib/buy/moonpay/moonpay_buy_provider.dart index 4ff3fb04c..372b6d6cc 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<String> requestUrl({required CryptoCurrency currency, required String refundWalletAddress}) async { + Future<Uri> requestUrl({required CryptoCurrency currency, required String refundWalletAddress}) async { final originalUri = Uri.https( baseUrl, '', <String, dynamic>{ 'apiKey': _apiKey, @@ -37,13 +37,13 @@ class MoonPaySellProvider { final signature = base64.encode(digest.bytes); if (isTest) { - return originalUri.toString(); + return originalUri; } final query = Map<String, dynamic>.from(originalUri.queryParameters); query['signature'] = signature; final signedUri = originalUri.replace(queryParameters: query); - return signedUri.toString(); + return signedUri; } } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart new file mode 100644 index 000000000..a887f98dc --- /dev/null +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -0,0 +1,69 @@ +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/wallet_base.dart'; + +class OnRamperBuyProvider { + OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) + : this._settingsStore = settingsStore, + this._wallet = wallet; + + final SettingsStore _settingsStore; + final WalletBase _wallet; + + static const _baseUrl = 'buy.onramper.com'; + + static String get _apiKey => secrets.onramperApiKey; + + Uri requestUrl() { + String primaryColor, + secondaryColor, + primaryTextColor, + secondaryTextColor, + containerColor, + cardColor; + + switch (_settingsStore.currentTheme.type) { + case ThemeType.bright: + primaryColor = '815dfbff'; + secondaryColor = 'ffffff'; + primaryTextColor = '141519'; + secondaryTextColor = '6b6f80'; + containerColor = 'ffffff'; + cardColor = 'f2f0faff'; + break; + case ThemeType.light: + primaryColor = '2194ffff'; + secondaryColor = 'ffffff'; + primaryTextColor = '141519'; + secondaryTextColor = '6b6f80'; + containerColor = 'ffffff'; + cardColor = 'e5f7ff'; + break; + case ThemeType.dark: + primaryColor = '456effff'; + secondaryColor = '1b2747ff'; + primaryTextColor = 'ffffff'; + secondaryTextColor = 'ffffff'; + containerColor = '19233C'; + cardColor = '232f4fff'; + break; + } + + + return Uri.https(_baseUrl, '', <String, dynamic>{ + 'apiKey': _apiKey, + 'defaultCrypto': _wallet.currency.title, + 'defaultFiat': _settingsStore.fiatCurrency.title, + 'wallets': '${_wallet.currency.title}:${_wallet.walletAddresses.address}', + 'supportSell': "false", + 'supportSwap': "false", + 'primaryColor': primaryColor, + 'secondaryColor': secondaryColor, + 'primaryTextColor': primaryTextColor, + 'secondaryTextColor': secondaryTextColor, + 'containerColor': containerColor, + 'cardColor': cardColor + }); + } +} diff --git a/lib/buy/payfura/payfura_buy_provider.dart b/lib/buy/payfura/payfura_buy_provider.dart new file mode 100644 index 000000000..eb9104df0 --- /dev/null +++ b/lib/buy/payfura/payfura_buy_provider.dart @@ -0,0 +1,24 @@ +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_base.dart'; + +class PayfuraBuyProvider { + PayfuraBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) + : this._settingsStore = settingsStore, + this._wallet = wallet; + + final SettingsStore _settingsStore; + final WalletBase _wallet; + + static const _baseUrl = 'exchange.payfura.com'; + + Uri requestUrl() { + return Uri.https(_baseUrl, '', <String, dynamic>{ + 'apiKey': secrets.payfuraApiKey, + 'to': _wallet.currency.title, + 'from': _settingsStore.fiatCurrency.title, + 'walletAddress': '${_wallet.currency.title}:${_wallet.walletAddresses.address}', + 'mode': 'buy' + }); + } +} diff --git a/lib/di.dart b/lib/di.dart index 166fa218f..2d6e1ec34 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,17 +1,22 @@ import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; -import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/buy/onramper_page.dart'; import 'package:cake_wallet/src/screens/buy/payfura_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; @@ -22,8 +27,11 @@ import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dar 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/settings/connection_sync_page.dart'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; @@ -143,7 +151,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -219,12 +226,16 @@ Future setup( () => SharedPreferences.getInstance()); } - final isBitcoinBuyEnabled = (secrets.wyreSecretKey?.isNotEmpty ?? false) && - (secrets.wyreApiKey?.isNotEmpty ?? false) && - (secrets.wyreAccountId?.isNotEmpty ?? false); + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) && + (secrets.wyreApiKey.isNotEmpty ?? false) && + (secrets.wyreAccountId.isNotEmpty ?? false); final settingsStore = await SettingsStoreBase.load( - nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled); + nodeSource: _nodeSource, + isBitcoinBuyEnabled: isBitcoinBuyEnabled, + // Enforce darkTheme on platforms other than mobile till the design for other themes is completed + initialTheme: DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme, + ); if (_isSetupFinished) { return; @@ -375,42 +386,64 @@ Future setup( .registerFactoryParam<AuthPage, void Function(bool, AuthPageState), bool>( (onAuthFinished, closable) => AuthPage(getIt.get<AuthViewModel>(), onAuthenticationFinished: onAuthFinished, - closable: closable ?? false)); + closable: closable)); getIt.registerFactory(() => BalancePage(dashboardViewModel: getIt.get<DashboardViewModel>(), settingsStore: getIt.get<SettingsStore>())); - getIt.registerFactory<DashboardPage>(() => DashboardPage( balancePage: getIt.get<BalancePage>(), walletViewModel: getIt.get<DashboardViewModel>(), addressListViewModel: getIt.get<WalletAddressListViewModel>())); - + getIt.registerFactory<DashboardPage>(() => DashboardPage( + balancePage: getIt.get<BalancePage>(), + dashboardViewModel: getIt.get<DashboardViewModel>(), + addressListViewModel: getIt.get<WalletAddressListViewModel>(), + )); + getIt.registerFactory<DesktopSidebarWrapper>(() { + final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); + return DesktopSidebarWrapper( + dashboardViewModel: getIt.get<DashboardViewModel>(), + desktopSidebarViewModel: getIt.get<DesktopSidebarViewModel>(), + child: getIt.get<DesktopDashboardPage>(param1: _navigatorKey), + desktopNavigatorKey: _navigatorKey, + ); + }); + getIt.registerFactoryParam<DesktopDashboardPage, GlobalKey<NavigatorState>, void>( + (desktopKey, _) => DesktopDashboardPage( + balancePage: getIt.get<BalancePage>(), + dashboardViewModel: getIt.get<DashboardViewModel>(), + addressListViewModel: getIt.get<WalletAddressListViewModel>(), + desktopKey: desktopKey, + )); + + getIt.registerFactory<TransactionsPage>(() => TransactionsPage(dashboardViewModel: getIt.get<DashboardViewModel>())); + getIt.registerFactoryParam<ReceiveOptionViewModel, ReceivePageOption?, void>((pageOption, _) => ReceiveOptionViewModel( getIt.get<AppStore>().wallet!, pageOption)); - getIt.registerFactoryParam<AnonInvoicePageViewModel, List<dynamic>, void>((args, _) { + getIt.registerFactoryParam<AnonInvoicePageViewModel, List<dynamic>, void>((args, _) { final address = args.first as String; final pageOption = args.last as ReceivePageOption; return AnonInvoicePageViewModel( - getIt.get<AnonPayApi>(), - address, - getIt.get<SettingsStore>(), - getIt.get<AppStore>().wallet!, - _anonpayInvoiceInfoSource, + getIt.get<AnonPayApi>(), + address, + getIt.get<SettingsStore>(), + getIt.get<AppStore>().wallet!, + _anonpayInvoiceInfoSource, getIt.get<SharedPreferences>(), pageOption, - ); + ); }); - getIt.registerFactoryParam<AnonPayInvoicePage, List<dynamic>, void>((List<dynamic> args, _) { + getIt.registerFactoryParam<AnonPayInvoicePage, List<dynamic>, void>((List<dynamic> args, _) { final pageOption = args.last as ReceivePageOption; return AnonPayInvoicePage( - getIt.get<AnonInvoicePageViewModel>(param1: args), + getIt.get<AnonInvoicePageViewModel>(param1: args), getIt.get<ReceiveOptionViewModel>(param1: pageOption)); - }); - + }); + getIt.registerFactory<ReceivePage>(() => ReceivePage( addressListViewModel: getIt.get<WalletAddressListViewModel>())); getIt.registerFactory<AddressPage>(() => AddressPage( addressListViewModel: getIt.get<WalletAddressListViewModel>(), - walletViewModel: getIt.get<DashboardViewModel>(), + dashboardViewModel: getIt.get<DashboardViewModel>(), receiveOptionViewModel: getIt.get<ReceiveOptionViewModel>())); getIt.registerFactoryParam<WalletAddressEditOrCreateViewModel, WalletAddressListItem?, void>( @@ -445,13 +478,25 @@ Future setup( getIt.registerFactory(() => SendTemplatePage( sendTemplateViewModel: getIt.get<SendTemplateViewModel>())); - getIt.registerFactory(() => WalletListViewModel( - _walletInfoSource, - getIt.get<AppStore>(), - getIt.get<WalletLoadingService>(), - getIt.get<AuthService>(), - ), - ); + if (DeviceInfo.instance.isMobile) { + getIt.registerFactory(() => WalletListViewModel( + _walletInfoSource, + getIt.get<AppStore>(), + getIt.get<WalletLoadingService>(), + getIt.get<AuthService>(), + ), + ); + } else { + // register wallet list view model as singleton on desktop since it can be accessed + // from multiple places at the same time (Wallets DropDown, Wallets List in settings) + getIt.registerLazySingleton(() => WalletListViewModel( + _walletInfoSource, + getIt.get<AppStore>(), + getIt.get<WalletLoadingService>(), + getIt.get<AuthService>(), + ), + ); + } getIt.registerFactory(() => WalletListPage(walletListViewModel: getIt.get<WalletListViewModel>())); @@ -523,7 +568,7 @@ Future setup( isNewWalletCreated: isWalletCreated)); getIt - .registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>().wallet!)); + .registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>())); getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>())); @@ -543,8 +588,7 @@ Future setup( getIt.registerFactory(() { final appStore = getIt.get<AppStore>(); - return NodeListViewModel( - _nodeSource, appStore.wallet!, appStore.settingsStore); + return NodeListViewModel(_nodeSource, appStore); }); getIt.registerFactory(() => ConnectionSyncPage(getIt.get<NodeListViewModel>(), getIt.get<DashboardViewModel>())); @@ -570,13 +614,19 @@ Future setup( editingNode: editingNode, isSelected: isSelected)); - getIt.registerFactory(() => OnRamperPage( + getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider( settingsStore: getIt.get<AppStore>().settingsStore, - wallet: getIt.get<AppStore>().wallet!)); + wallet: getIt.get<AppStore>().wallet!, + )); - getIt.registerFactory(() => PayFuraPage( - settingsStore: getIt.get<AppStore>().settingsStore, - wallet: getIt.get<AppStore>().wallet!)); + getIt.registerFactory(() => OnRamperPage(getIt.get<OnRamperBuyProvider>())); + + getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider( + settingsStore: getIt.get<AppStore>().settingsStore, + wallet: getIt.get<AppStore>().wallet!, + )); + + getIt.registerFactory(() => PayFuraPage(getIt.get<PayfuraBuyProvider>())); getIt.registerFactory(() => ExchangeViewModel( getIt.get<AppStore>().wallet!, @@ -759,8 +809,6 @@ Future setup( param1: item, param2: unspentCoinsListViewModel)); }); - getIt.registerFactory(() => WakeLock()); - getIt.registerFactory(() => YatService()); getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(), @@ -873,11 +921,15 @@ Future setup( getIt.registerFactory(() => IoniaAccountPage(getIt.get<IoniaAccountViewModel>())); getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>())); - + getIt.registerFactory(() => AnonPayApi(useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly, - wallet: getIt.get<AppStore>().wallet!) + wallet: getIt.get<AppStore>().wallet!) ); + getIt.registerFactory(() => DesktopWalletSelectionDropDown(getIt.get<WalletListViewModel>())); + + getIt.registerFactory(() => DesktopSidebarViewModel()); + getIt.registerFactoryParam<AnonpayDetailsViewModel, AnonpayInvoiceInfo, void>( (AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsViewModel( @@ -890,7 +942,7 @@ Future setup( (AnonpayInfoBase anonpayInvoiceInfo, _) => AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo)); getIt.registerFactoryParam<AnonpayDetailsPage, AnonpayInvoiceInfo, void>( - (AnonpayInvoiceInfo anonpayInvoiceInfo, _) + (AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo))); getIt.registerFactoryParam<IoniaPaymentStatusViewModel, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>( diff --git a/lib/entities/desktop_dropdown_item.dart b/lib/entities/desktop_dropdown_item.dart new file mode 100644 index 000000000..3a542f5e8 --- /dev/null +++ b/lib/entities/desktop_dropdown_item.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class DesktopDropdownItem { + final Function() onSelected; + final Widget child; + final bool isSelected; + + DesktopDropdownItem({required this.onSelected, required this.child, this.isSelected = false}); +} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart new file mode 100644 index 000000000..ec70b95d9 --- /dev/null +++ b/lib/entities/main_actions.dart @@ -0,0 +1,142 @@ +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MainActions { + final String Function(BuildContext context) name; + final String image; + + final bool Function(DashboardViewModel viewModel)? isEnabled; + final bool Function(DashboardViewModel viewModel)? canShow; + final Future<void> Function(BuildContext context, DashboardViewModel viewModel) onTap; + + MainActions._({ + required this.name, + required this.image, + this.isEnabled, + this.canShow, + required this.onTap, + }); + + static List<MainActions> all = [ + buyAction, + receiveAction, + exchangeAction, + sendAction, + sellAction, + ]; + + static MainActions buyAction = MainActions._( + name: (context) => S.of(context).buy, + image: 'assets/images/buy.png', + isEnabled: (viewModel) => viewModel.isEnabledBuyAction, + canShow: (viewModel) => viewModel.hasBuyAction, + onTap: (BuildContext context, DashboardViewModel viewModel) async { + final walletType = viewModel.type; + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.onramperPage); + } else { + final uri = getIt + .get<OnRamperBuyProvider>() + .requestUrl(); + await launchUrl(uri); + } + break; + case WalletType.monero: + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.payfuraPage); + } else { + final uri = getIt + .get<PayfuraBuyProvider>() + .requestUrl(); + await launchUrl(uri); + } + break; + default: + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).buy, + alertContent: S.of(context).buy_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + }, + ); + + static MainActions receiveAction = MainActions._( + name: (context) => S.of(context).receive, + image: 'assets/images/received.png', + onTap: (BuildContext context, DashboardViewModel viewModel) async { + Navigator.pushNamed(context, Routes.addressPage); + }, + ); + + static MainActions exchangeAction = MainActions._( + name: (context) => S.of(context).exchange, + image: 'assets/images/transfer.png', + isEnabled: (viewModel) => viewModel.isEnabledExchangeAction, + canShow: (viewModel) => viewModel.hasExchangeAction, + onTap: (BuildContext context, DashboardViewModel viewModel) async { + if (viewModel.isEnabledExchangeAction) { + await Navigator.of(context).pushNamed(Routes.exchange); + } + }, + ); + + static MainActions sendAction = MainActions._( + name: (context) => S.of(context).send, + image: 'assets/images/upload.png', + onTap: (BuildContext context, DashboardViewModel viewModel) async { + Navigator.pushNamed(context, Routes.send); + }, + ); + + static MainActions sellAction = MainActions._( + name: (context) => S.of(context).sell, + image: 'assets/images/sell.png', + isEnabled: (viewModel) => viewModel.isEnabledSellAction, + canShow: (viewModel) => viewModel.hasSellAction, + onTap: (BuildContext context, DashboardViewModel viewModel) async { + final walletType = viewModel.type; + + switch (walletType) { + case WalletType.bitcoin: + final moonPaySellProvider = MoonPaySellProvider(); + final uri = await moonPaySellProvider.requestUrl( + currency: viewModel.wallet.currency, + refundWalletAddress: viewModel.wallet.walletAddresses.address, + ); + await launchUrl(uri); + break; + default: + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).sell, + alertContent: S.of(context).sell_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }, + ); + } + }, + ); +} diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index ab94e31fb..c5ec71ab5 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/device_info.dart'; import 'package:flutter/services.dart'; const channel = MethodChannel('com.cake_wallet/native_utils'); @@ -6,13 +7,18 @@ Future<String> fetchUnstoppableDomainAddress(String domain, String ticker) async var address = ''; try { - address = await channel.invokeMethod<String>( - 'getUnstoppableDomainAddress', - <String, String> { - 'domain' : domain, - 'ticker' : ticker - } - ) ?? ''; + if (DeviceInfo.instance.isMobile) { + address = await channel.invokeMethod<String>( + 'getUnstoppableDomainAddress', + <String, String> { + 'domain' : domain, + 'ticker' : ticker + } + ) ?? ''; + } else { + // TODO: Integrate with Unstoppable domains resolution API + return address; + } } catch (e) { print('Unstoppable domain error: ${e.toString()}'); address = ''; diff --git a/lib/entities/wake_lock.dart b/lib/entities/wake_lock.dart deleted file mode 100644 index 99acc65ee..000000000 --- a/lib/entities/wake_lock.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/services.dart'; - -class WakeLock { - static const _utils = const MethodChannel('com.cake_wallet/native_utils'); - - Future<void> enableWake() async { - try { - await _utils.invokeMethod<bool>('enableWakeScreen'); - } on PlatformException catch (_) { - print('Failed enabling screen wakelock'); - } - } - - Future<void> disableWake() async { - try { - await _utils.invokeMethod<bool>('disableWakeScreen'); - } on PlatformException catch (_) { - print('Failed enabling screen wakelock'); - } - } -} diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index e173dbdf6..89d32fa09 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cw_core/crypto_currency.dart'; @@ -24,7 +25,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { .expand((i) => i) .toList()); - static const apiKey = secrets.changeNowApiKey; + static final apiKey = DeviceInfo.instance.isMobile ? secrets.changeNowApiKey : secrets.changeNowApiKeyDesktop; static const apiAuthority = 'api.changenow.io'; static const createTradePath = '/v2/exchange'; static const findTradeByIdPath = '/v2/exchange/by-id'; diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart index 6c5c23460..4c1072d11 100644 --- a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart +++ b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart @@ -6,6 +6,7 @@ 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:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -29,7 +30,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { static const rangePath = '/v1/get_ranges'; static const getExchangePath = '/v1/get_exchange'; static const createExchangePath = '/v1/create_exchange'; - static const apiKey = secrets.simpleSwapApiKey; + static final apiKey = DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; @override ExchangeProviderDescription get description => diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 7521170e6..5f1214b76 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -9,8 +9,8 @@ ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; -void startAuthenticationStateChange(AuthenticationStore authenticationStore, - GlobalKey<NavigatorState> navigatorKey) { +void startAuthenticationStateChange( + AuthenticationStore authenticationStore, GlobalKey<NavigatorState> navigatorKey) { _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 68bd4b3c2..767bfd7e8 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,6 +1,4 @@ -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; -import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -9,7 +7,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/sync_status.dart'; -import 'package:flutter/services.dart'; +import 'package:wakelock/wakelock.dart'; ReactionDisposer? _onWalletSyncStatusChangeReaction; @@ -17,7 +15,6 @@ void startWalletSyncStatusChangeReaction( WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet, FiatConversionStore fiatConversionStore) { - final _wakeLock = getIt.get<WakeLock>(); _onWalletSyncStatusChangeReaction?.reaction.dispose(); _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { @@ -30,10 +27,10 @@ void startWalletSyncStatusChangeReaction( } } if (status is SyncingSyncStatus) { - await _wakeLock.enableWake(); + await Wakelock.enable(); } if (status is SyncedSyncStatus || status is FailedSyncStatus) { - await _wakeLock.disableWake(); + await Wakelock.disable(); } } catch(e) { print(e.toString()); diff --git a/lib/router.dart b/lib/router.dart index 634a69966..aebee0942 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -11,6 +11,9 @@ import 'package:cake_wallet/src/screens/buy/payfura_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; +import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; @@ -32,6 +35,7 @@ import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; @@ -295,22 +299,27 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.connectionSync: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>()); case Routes.securityBackupPage: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<SecurityBackupPage>()); case Routes.privacyPage: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<PrivacyPage>()); case Routes.displaySettingsPage: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<DisplaySettingsPage>()); case Routes.otherSettingsPage: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<OtherSettingsPage>()); case Routes.newNode: @@ -336,8 +345,8 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.addressBook: return MaterialPageRoute<void>( - builder: (_) => - getIt.get<ContactListPage>()); + fullscreenDialog: true, + builder: (_) => getIt.get<ContactListPage>()); case Routes.pickerAddressBook: final selectedCurrency = settings.arguments as CryptoCurrency?; @@ -394,6 +403,7 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.exchange: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<ExchangePage>()); case Routes.exchangeTemplate: @@ -425,6 +435,7 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.support: return CupertinoPageRoute<void>( + fullscreenDialog: true, builder: (_) => getIt.get<SupportPage>()); case Routes.unspentCoinsList: @@ -447,11 +458,14 @@ Route<dynamic> createRoute(RouteSettings settings) { getIt.get<FullscreenQRPage>( param1: args['qrData'] as String, param2: args['version'] as int?, - + )); case Routes.ioniaWelcomePage: - return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaWelcomePage>()); + return CupertinoPageRoute<void>( + fullscreenDialog: true, + builder: (_) => getIt.get<IoniaWelcomePage>(), + ); case Routes.ioniaLoginPage: return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaLoginPage>()); @@ -524,19 +538,39 @@ Route<dynamic> createRoute(RouteSettings settings) { getIt.get<AdvancedPrivacySettingsViewModel>(param1: type), getIt.get<NodeCreateOrEditViewModel>(param1: type), )); - + case Routes.anonPayInvoicePage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonPayInvoicePage>(param1: args)); - + case Routes.anonPayReceivePage: final anonInvoiceViewData = settings.arguments as AnonpayInfoBase; return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonPayReceivePage>(param1: anonInvoiceViewData)); - + case Routes.anonPayDetailsPage: final anonInvoiceViewData = settings.arguments as AnonpayInvoiceInfo; return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData)); - + + case Routes.desktop_actions: + return PageRouteBuilder( + opaque: false, + pageBuilder: (_, __, ___) => DesktopDashboardActions(getIt<DashboardViewModel>()), + ); + + case Routes.desktop_settings_page: + return CupertinoPageRoute<void>( + builder: (_) => DesktopSettingsPage()); + + case Routes.empty_no_route: + return MaterialPageRoute<void>( + builder: (_) => SizedBox.shrink()); + + case Routes.transactionsPage: + return CupertinoPageRoute<void>( + settings: settings, + fullscreenDialog: true, + builder: (_) => getIt.get<TransactionsPage>()); + default: return MaterialPageRoute<void>( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 99d0f438e..295b55ae0 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -37,6 +37,8 @@ class Routes { static const restoreWalletFromSeedDetails = '/restore_from_seed_details'; static const exchange = '/exchange'; static const settings = '/settings'; + static const desktop_settings_page = '/desktop_settings_page'; + static const empty_no_route = '/empty_no_route'; static const unlock = '/auth_not_closable'; static const rescan = '/rescan'; static const faq = '/faq'; @@ -86,4 +88,6 @@ class Routes { static const anonPayReceivePage = '/anon_pay_receive_page'; static const anonPayDetailsPage = '/anon_pay_details_page'; static const payfuraPage = '/pay_fura_page'; + static const desktop_actions = '/desktop_actions'; + static const transactionsPage = '/transactions_page'; } diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart index f819e88e5..966b289d1 100644 --- a/lib/src/screens/backup/backup_page.dart +++ b/lib/src/screens/backup/backup_page.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/utils/share_util.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -27,8 +29,7 @@ class BackupPage extends BasePage { @override Widget trailing(BuildContext context) => TrailButton( caption: S.of(context).change_password, - onPressed: () => - Navigator.of(context).pushNamed(Routes.editBackupPassword), + onPressed: () => Navigator.of(context).pushNamed(Routes.editBackupPassword), textColor: Palette.blueCraiola); @override @@ -51,9 +52,8 @@ class BackupPage extends BasePage { child: Observer( builder: (_) => GestureDetector( onTap: () { - Clipboard.setData(ClipboardData( - text: - backupViewModelBase.backupPassword)); + Clipboard.setData( + ClipboardData(text: backupViewModelBase.backupPassword)); showBar<void>( context, S.of(context).transaction_details_copied( @@ -108,8 +108,10 @@ class BackupPage extends BasePage { if (Platform.isAndroid) { onExportAndroid(context, backup); - } else { + } else if (Platform.isIOS) { await share(backup, context); + } else { + await _saveFile(backup); } }, actionLeftButton: () => Navigator.of(dialogContext).pop()); @@ -133,8 +135,7 @@ class BackupPage extends BasePage { return; } - await backupViewModelBase.saveToDownload( - backup.name, backup.content); + await backupViewModelBase.saveToDownload(backup.name, backup.content); Navigator.of(dialogContext).pop(); }, actionLeftButton: () async { @@ -149,4 +150,20 @@ class BackupPage extends BasePage { await ShareUtil.shareFile(filePath: path, fileName: backup.name, context: context); await backupViewModelBase.removeBackupFileLocally(backup); } + + Future<void> _saveFile(BackupExportFile backup) async { + String? outputFile = await FilePicker.platform + .saveFile(dialogTitle: 'Save Your File to desired location', fileName: backup.name); + + try { + File returnedFile = File(outputFile!); + await returnedFile.writeAsBytes(backup.content); + } catch (exception, stackTrace) { + ExceptionHandler.onError(FlutterErrorDetails( + exception: exception, + stack: stackTrace, + library: "Export Backup", + )); + } + } } diff --git a/lib/src/screens/backup/edit_backup_password_page.dart b/lib/src/screens/backup/edit_backup_password_page.dart index 47ae77c0e..f1ddaf4e1 100644 --- a/lib/src/screens/backup/edit_backup_password_page.dart +++ b/lib/src/screens/backup/edit_backup_password_page.dart @@ -66,7 +66,8 @@ class EditBackupPasswordPage extends BasePage { leftButtonText: S.of(context).cancel, actionRightButton: () async { await editBackupPasswordViewModel.save(); - Navigator.of(dialogContext)..pop()..pop(); + Navigator.of(dialogContext).pop(); + Navigator.of(context).pop(); }, actionLeftButton: () => Navigator.of(dialogContext).pop()); }); diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 92bf56eaf..123eef65c 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; @@ -20,7 +21,7 @@ abstract class BasePage extends StatelessWidget { String? get title => null; - bool get isModalBackButton => false; + bool get canUseCloseIcon => false; Color get backgroundLightColor => Colors.white; @@ -50,23 +51,27 @@ abstract class BasePage extends StatelessWidget { } final _backButton = Icon(Icons.arrow_back_ios, - color: titleColor ?? Theme.of(context).primaryTextTheme!.headline6!.color!, + color: titleColor ?? Theme.of(context).primaryTextTheme.headline6!.color!, size: 16,); final _closeButton = currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); + return SizedBox( - height: 37, - width: 37, + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, child: ButtonTheme( minWidth: double.minPositive, child: TextButton( - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + ), onPressed: () => onClose(context), - child: isModalBackButton ? _closeButton : _backButton), + child: canUseCloseIcon && !isMobileView + ? _closeButton + : _backButton, + ), ), ); } @@ -92,7 +97,7 @@ abstract class BasePage extends StatelessWidget { ObstructingPreferredSizeWidget appBar(BuildContext context) { final appBarColor = currentTheme.type == ThemeType.dark ? backgroundDarkColor : backgroundLightColor; - + switch (appBarStyle) { case AppBarStyle.regular: // FIX-ME: NavBar no context diff --git a/lib/src/screens/buy/onramper_page.dart b/lib/src/screens/buy/onramper_page.dart index 4973cef0b..cbdf9e1b1 100644 --- a/lib/src/screens/buy/onramper_page.dart +++ b/lib/src/screens/buy/onramper_page.dart @@ -1,60 +1,30 @@ +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; class OnRamperPage extends BasePage { - OnRamperPage({required this.settingsStore, required this.wallet}); + OnRamperPage(this._onRamperBuyProvider); - final SettingsStore settingsStore; - final WalletBase wallet; + final OnRamperBuyProvider _onRamperBuyProvider; @override String get title => S.current.buy; @override Widget body(BuildContext context) { - final darkMode = Theme.of(context).brightness == Brightness.dark; - return OnRamperPageBody( - settingsStore: settingsStore, - wallet: wallet, - darkMode: darkMode, - backgroundColor: darkMode ? backgroundDarkColor : backgroundLightColor, - supportSell: false, - supportSwap: false); + return OnRamperPageBody(_onRamperBuyProvider); } } class OnRamperPageBody extends StatefulWidget { - OnRamperPageBody( - {required this.settingsStore, - required this.wallet, - required this.darkMode, - required this.supportSell, - required this.supportSwap, - required this.backgroundColor}); + OnRamperPageBody(this.onRamperBuyProvider); - static const baseUrl = 'widget.onramper.com'; - final SettingsStore settingsStore; - final WalletBase wallet; - final Color backgroundColor; - final bool darkMode; - final bool supportSell; - final bool supportSwap; + final OnRamperBuyProvider onRamperBuyProvider; - Uri get uri => Uri.https(baseUrl, '', <String, dynamic>{ - 'apiKey': secrets.onramperApiKey, - 'defaultCrypto': wallet.currency.title, - 'defaultFiat': settingsStore.fiatCurrency.title, - 'wallets': '${wallet.currency.title}:${wallet.walletAddresses.address}', - 'darkMode': darkMode.toString(), - 'supportSell': supportSell.toString(), - 'supportSwap': supportSwap.toString() - }); + Uri get uri => onRamperBuyProvider.requestUrl(); @override OnRamperPageBodyState createState() => OnRamperPageBodyState(); diff --git a/lib/src/screens/buy/payfura_page.dart b/lib/src/screens/buy/payfura_page.dart index 4bada4a9e..a974aec25 100644 --- a/lib/src/screens/buy/payfura_page.dart +++ b/lib/src/screens/buy/payfura_page.dart @@ -1,45 +1,30 @@ +import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; class PayFuraPage extends BasePage { - PayFuraPage({required this.settingsStore, required this.wallet}); + PayFuraPage(this._PayfuraBuyProvider); - final SettingsStore settingsStore; - final WalletBase wallet; + final PayfuraBuyProvider _PayfuraBuyProvider; @override String get title => S.current.buy; @override Widget body(BuildContext context) { - return PayFuraPageBody( - settingsStore: settingsStore, - wallet: wallet); + return PayFuraPageBody(_PayfuraBuyProvider); } } class PayFuraPageBody extends StatefulWidget { - PayFuraPageBody( - {required this.settingsStore, - required this.wallet}); + PayFuraPageBody(this._PayfuraBuyProvider); - static const baseUrl = 'exchange.payfura.com'; - final SettingsStore settingsStore; - final WalletBase wallet; + final PayfuraBuyProvider _PayfuraBuyProvider; - Uri get uri => Uri.https(baseUrl, '', <String, dynamic>{ - 'apiKey': secrets.payfuraApiKey, - 'to': wallet.currency.title, - 'from': settingsStore.fiatCurrency.title, - 'walletAddress': '${wallet.currency.title}:${wallet.walletAddresses.address}', - 'mode': 'buy' - }); + Uri get uri => _PayfuraBuyProvider.requestUrl(); @override PayFuraPageBodyState createState() => PayFuraPageBodyState(); diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 0065b9281..f8ccf5807 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -24,10 +24,6 @@ class ContactListPage extends BasePage { @override Widget? trailing(BuildContext context) { - if (!contactListViewModel.isEditable) { - return null; - } - return Container( width: 32.0, height: 32.0, diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index b0df7f36c..d0e947508 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/main_actions.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -21,17 +24,41 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -class DashboardPage extends BasePage { +class DashboardPage extends StatelessWidget { DashboardPage({ required this.balancePage, - required this.walletViewModel, + required this.dashboardViewModel, required this.addressListViewModel, }); + final BalancePage balancePage; - + final DashboardViewModel dashboardViewModel; + final WalletAddressListViewModel addressListViewModel; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ResponsiveLayoutUtil.instance.isMobile(context) + ? _DashboardPageView( + balancePage: balancePage, + dashboardViewModel: dashboardViewModel, + addressListViewModel: addressListViewModel, + ) + : getIt.get<DesktopSidebarWrapper>(), + ); + } +} + +class _DashboardPageView extends BasePage { + _DashboardPageView({ + required this.balancePage, + required this.dashboardViewModel, + required this.addressListViewModel, + }); + + final BalancePage balancePage; + @override Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @@ -54,19 +81,19 @@ class DashboardPage extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(walletViewModel); + Widget get endDrawer => MenuWidget(dashboardViewModel); @override Widget middle(BuildContext context) { - return SyncIndicator(dashboardViewModel: walletViewModel, - onTap: () => Navigator.of(context, rootNavigator: true) - .pushNamed(Routes.connectionSync)); + return SyncIndicator( + dashboardViewModel: dashboardViewModel, + onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync)); } @override Widget trailing(BuildContext context) { final menuButton = Image.asset('assets/images/menu.png', - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!); return Container( alignment: Alignment.centerRight, @@ -80,7 +107,7 @@ class DashboardPage extends BasePage { child: menuButton)); } - final DashboardViewModel walletViewModel; + final DashboardViewModel dashboardViewModel; final WalletAddressListViewModel addressListViewModel; final controller = PageController(initialPage: 1); @@ -90,141 +117,97 @@ class DashboardPage extends BasePage { @override Widget body(BuildContext context) { - final sendImage = Image.asset('assets/images/upload.png', - height: 24, - width: 24, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); - final receiveImage = Image.asset('assets/images/received.png', - height: 24, - width: 24, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); _setEffects(context); return SafeArea( - minimum: EdgeInsets.only(bottom: 24), + minimum: EdgeInsets.only(bottom: 24), child: Column( - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - Expanded( - child: PageView.builder( - controller: controller, - itemCount: pages.length, - itemBuilder: (context, index) => pages[index])), - Padding( - padding: EdgeInsets.only(bottom: 24, top: 10), - child: SmoothPageIndicator( - controller: controller, - count: pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).indicatorColor, - activeDotColor: Theme.of(context) - .accentTextTheme! - .headline4! - .backgroundColor!), - )), - Observer(builder: (_) { - return ClipRect( - child:Container( - margin: const EdgeInsets.only(left: 16, right: 16), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.0), - border: Border.all(color: currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), - color:Theme.of(context).textTheme!.headline6!.backgroundColor!), + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + Expanded( + child: PageView.builder( + controller: controller, + itemCount: pages.length, + itemBuilder: (context, index) => pages[index])), + Padding( + padding: EdgeInsets.only(bottom: 24, top: 10), + child: SmoothPageIndicator( + controller: controller, + count: pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).indicatorColor, + activeDotColor: + Theme.of(context).accentTextTheme!.headline4!.backgroundColor!), + )), + Observer(builder: (_) { + return ClipRect( child: Container( - padding: EdgeInsets.only(left: 32, right: 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - if (walletViewModel.hasBuyAction) - ActionButton( - image: Image.asset('assets/images/buy.png', - height: 24, - width: 24, - color: !walletViewModel.isEnabledBuyAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), - title: S.of(context).buy, - onClick: () async => await _onClickBuyButton(context), - textColor: !walletViewModel.isEnabledBuyAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : null), - ActionButton( - image: receiveImage, - title: S.of(context).receive, - route: Routes.addressPage), - if (walletViewModel.hasExchangeAction) - ActionButton( - image: Image.asset('assets/images/transfer.png', - height: 24, - width: 24, - color: !walletViewModel.isEnabledExchangeAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), - title: S.of(context).exchange, - onClick: () async => _onClickExchangeButton(context), - textColor: !walletViewModel.isEnabledExchangeAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : null), - ActionButton( - image: sendImage, - title: S.of(context).send, - route: Routes.send), - if (walletViewModel.hasSellAction) - ActionButton( - image: Image.asset('assets/images/sell.png', - height: 24, - width: 24, - color: !walletViewModel.isEnabledSellAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), - title: S.of(context).sell, - onClick: () async => await _onClickSellButton(context), - textColor: !walletViewModel.isEnabledSellAction - ? Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor! - : null), - ], - ),), - ),),); - }), - - ], - )); + margin: const EdgeInsets.only(left: 16, right: 16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + border: Border.all( + color: currentTheme.type == ThemeType.bright + ? Color.fromRGBO(255, 255, 255, 0.2) + : Colors.transparent, + width: 1, + ), + color: Theme.of(context).textTheme.headline6!.backgroundColor!, + ), + child: Container( + padding: EdgeInsets.only(left: 32, right: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: MainActions.all + .where((element) => element.canShow?.call(dashboardViewModel) ?? true) + .map((action) => ActionButton( + image: Image.asset(action.image, + height: 24, + width: 24, + color: action.isEnabled?.call(dashboardViewModel) ?? true + ? Theme.of(context) + .accentTextTheme + .headline2! + .backgroundColor! + : Theme.of(context) + .accentTextTheme + .headline3! + .backgroundColor!), + title: action.name(context), + onClick: () async => await action.onTap(context, dashboardViewModel), + textColor: action.isEnabled?.call(dashboardViewModel) ?? true + ? null + : Theme.of(context) + .accentTextTheme + .headline3! + .backgroundColor!, + )) + .toList(), + ), + ), + ), + ), + ); + }), + ], + )); } void _setEffects(BuildContext context) async { if (_isEffectsInstalled) { return; } - pages.add(MarketPlacePage(dashboardViewModel: walletViewModel)); + pages.add(MarketPlacePage(dashboardViewModel: dashboardViewModel)); pages.add(balancePage); - pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); + pages.add(TransactionsPage(dashboardViewModel: dashboardViewModel)); _isEffectsInstalled = true; autorun((_) async { - if (!walletViewModel.isOutdatedElectrumWallet) { + if (!dashboardViewModel.isOutdatedElectrumWallet) { return; } @@ -235,8 +218,7 @@ class DashboardPage extends BasePage { builder: (BuildContext context) { return AlertWithOneAction( alertTitle: S.of(context).pre_seed_title, - alertContent: - S.of(context).outdated_electrum_wallet_description, + alertContent: S.of(context).outdated_electrum_wallet_description, buttonText: S.of(context).understand, buttonAction: () => Navigator.of(context).pop()); }); @@ -253,13 +235,13 @@ class DashboardPage extends BasePage { Future<void>.delayed(Duration(milliseconds: 500)).then((_) { showPopUp<void>( context: navigatorKey.currentContext!, - builder: (_) => YatEmojiId(walletViewModel.yatStore.emoji)); + builder: (_) => YatEmojiId(dashboardViewModel.yatStore.emoji)); needToPresentYat = false; }); } }); - walletViewModel.yatStore.emojiIncommingStream.listen((String emoji) { + dashboardViewModel.yatStore.emojiIncommingStream.listen((String emoji) { if (!_isEffectsInstalled || emoji.isEmpty) { return; } @@ -267,62 +249,4 @@ class DashboardPage extends BasePage { needToPresentYat = true; }); } - - Future<void> _onClickBuyButton(BuildContext context) async { - final walletType = walletViewModel.type; - - switch (walletType) { - case WalletType.bitcoin: - Navigator.of(context).pushNamed(Routes.onramperPage); - break; - case WalletType.litecoin: - Navigator.of(context).pushNamed(Routes.onramperPage); - break; - case WalletType.monero: - Navigator.of(context).pushNamed(Routes.payfuraPage); - break; - default: - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).buy, - alertContent: S.of(context).buy_alert_content, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - } - - Future<void> _onClickSellButton(BuildContext context) async { - final walletType = walletViewModel.type; - - switch (walletType) { - case WalletType.bitcoin: - final moonPaySellProvider = MoonPaySellProvider(); - final uri = await moonPaySellProvider.requestUrl( - currency: walletViewModel.wallet.currency, - refundWalletAddress: - walletViewModel.wallet.walletAddresses.address); - await launch(uri); - break; - default: - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).sell, - alertContent: isMoneroOnly ? S.of(context).sell_monero_com_alert_content - : S.of(context).sell_alert_content, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - } - - Future<void> _onClickExchangeButton(BuildContext context) async { - if (walletViewModel.isEnabledExchangeAction) { - await Navigator.of(context).pushNamed(Routes.exchange); - } - } } diff --git a/lib/src/screens/dashboard/desktop_dashboard_page.dart b/lib/src/screens/dashboard/desktop_dashboard_page.dart new file mode 100644 index 000000000..64f8a9aac --- /dev/null +++ b/lib/src/screens/dashboard/desktop_dashboard_page.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/router.dart' as Router; + +class DesktopDashboardPage extends StatelessWidget { + DesktopDashboardPage({ + required this.balancePage, + required this.dashboardViewModel, + required this.addressListViewModel, + required this.desktopKey, + }); + + final BalancePage balancePage; + final DashboardViewModel dashboardViewModel; + final WalletAddressListViewModel addressListViewModel; + final GlobalKey<NavigatorState> desktopKey; + + bool _isEffectsInstalled = false; + StreamSubscription<bool>? _onInactiveSub; + + @override + Widget build(BuildContext context) { + _setEffects(context); + + return Container( + color: Theme.of(context).backgroundColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 400, + child: balancePage, + ), + Flexible( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: Navigator( + key: desktopKey, + initialRoute: Routes.desktop_actions, + onGenerateRoute: (settings) => Router.createRoute(settings), + onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { + return [ + navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! + ]; + }, + ), + ), + ), + ], + ), + ); + } + + void _setEffects(BuildContext context) async { + if (_isEffectsInstalled) { + return; + } + _isEffectsInstalled = true; + + autorun((_) async { + if (!dashboardViewModel.isOutdatedElectrumWallet) { + return; + } + + await Future<void>.delayed(Duration(seconds: 1)); + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).pre_seed_title, + alertContent: S.of(context).outdated_electrum_wallet_description, + buttonText: S.of(context).understand, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + + var needToPresentYat = false; + var isInactive = false; + + _onInactiveSub = rootKey.currentState!.isInactive.listen((inactive) { + isInactive = inactive; + + if (needToPresentYat) { + Future<void>.delayed(Duration(milliseconds: 500)).then((_) { + showPopUp<void>( + context: navigatorKey.currentContext!, + builder: (_) => YatEmojiId(dashboardViewModel.yatStore.emoji)); + needToPresentYat = false; + }); + } + }); + + dashboardViewModel.yatStore.emojiIncommingStream.listen((String emoji) { + if (!_isEffectsInstalled || emoji.isEmpty) { + return; + } + + needToPresentYat = true; + }); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart new file mode 100644 index 000000000..0e3588a17 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_action_button.dart @@ -0,0 +1,69 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; + +class DesktopActionButton extends StatelessWidget { + final String image; + final String title; + final bool canShow; + final bool isEnabled; + final Function() onTap; + + const DesktopActionButton({ + Key? key, + required this.title, + required this.image, + required this.onTap, + bool? canShow, + bool? isEnabled, + }) : this.isEnabled = isEnabled ?? true, + this.canShow = canShow ?? true, + super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(vertical: 25), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + color: Theme.of(context).textTheme.headline6!.backgroundColor!, + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + image, + height: 30, + width: 30, + color: isEnabled + ? Theme.of(context).accentTextTheme.headline2!.backgroundColor! + : Theme.of(context).accentTextTheme.headline3!.backgroundColor!, + ), + const SizedBox(width: 10), + AutoSizeText( + title, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + color: isEnabled + ? Theme.of(context).accentTextTheme.headline2!.backgroundColor! + : null, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart new file mode 100644 index 000000000..e5fa42390 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -0,0 +1,80 @@ +import 'package:cake_wallet/entities/main_actions.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DesktopDashboardActions extends StatelessWidget { + final DashboardViewModel dashboardViewModel; + + const DesktopDashboardActions(this.dashboardViewModel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) { + return Column( + children: [ + const SizedBox(height: 16), + DesktopActionButton( + title: MainActions.exchangeAction.name(context), + image: MainActions.exchangeAction.image, + canShow: MainActions.exchangeAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.exchangeAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.exchangeAction.onTap(context, dashboardViewModel), + ), + Row( + children: [ + Expanded( + child: DesktopActionButton( + title: MainActions.receiveAction.name(context), + image: MainActions.receiveAction.image, + canShow: MainActions.receiveAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.receiveAction.isEnabled?.call(dashboardViewModel), + onTap: () async => + await MainActions.receiveAction.onTap(context, dashboardViewModel), + ), + ), + Expanded( + child: DesktopActionButton( + title: MainActions.sendAction.name(context), + image: MainActions.sendAction.image, + canShow: MainActions.sendAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.sendAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.sendAction.onTap(context, dashboardViewModel), + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: DesktopActionButton( + title: MainActions.buyAction.name(context), + image: MainActions.buyAction.image, + canShow: MainActions.buyAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel), + ), + ), + Expanded( + child: DesktopActionButton( + title: MainActions.sellAction.name(context), + image: MainActions.sellAction.image, + canShow: MainActions.sellAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel), + ), + ), + ], + ), + Expanded( + child: MarketPlacePage(dashboardViewModel: dashboardViewModel), + ), + ], + ); + } + ); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart new file mode 100644 index 000000000..b97def132 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart @@ -0,0 +1,48 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class DesktopDashboardNavbar extends StatelessWidget implements ObstructingPreferredSizeWidget { + final Widget leading; + final Widget middle; + final Widget trailing; + + DesktopDashboardNavbar({ + super.key, + required this.leading, + required this.middle, + required this.trailing, + }); + + ThemeBase get currentTheme => getIt.get<SettingsStore>().currentTheme; + + @override + Widget build(BuildContext context) { + final appBarColor = + currentTheme.type == ThemeType.dark ? Colors.black.withOpacity(0.1) : Colors.white; + + return Container( + padding: const EdgeInsetsDirectional.only(end: 24), + color: appBarColor, + child: Center( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: leading), + middle, + trailing, + ], + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(60); + + @override + bool shouldFullyObstruct(BuildContext context) => false; +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart new file mode 100644 index 000000000..bc7c0af84 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart'; +import 'package:flutter/material.dart'; + +class SideMenu extends StatelessWidget { + const SideMenu({ + super.key, + required this.topItems, + required this.bottomItems, + required this.width, + }); + + final List<SideMenuItem> topItems; + final List<SideMenuItem> bottomItems; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.black.withOpacity(0.1), + width: width, + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + ...topItems, + Spacer(), + ...bottomItems, + SizedBox(height: 30), + ], + ), + ); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart new file mode 100644 index 000000000..42f4b0e4e --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class SideMenuItem extends StatelessWidget { + const SideMenuItem({ + Key? key, + required this.onTap, + this.imagePath, + this.icon, + this.isSelected = false, + }) : assert((icon != null && imagePath == null) || (icon == null && imagePath != null)); + + final void Function() onTap; + final String? imagePath; + final IconData? icon; + final bool isSelected; + + Color _setColor(BuildContext context) { + if (isSelected) { + return Theme.of(context).primaryTextTheme.headline6!.color!; + } else { + return Theme.of(context).highlightColor; + } + } + + @override + Widget build(BuildContext context) { + return InkWell( + child: Padding( + padding: EdgeInsets.all(20), + child: icon != null + ? Icon( + icon, + color: _setColor(context), + ) + : Image.asset( + imagePath ?? '', + fit: BoxFit.cover, + height: 30, + width: 30, + color: _setColor(context), + ), + ), + onTap: () => onTap.call(), + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + ); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart new file mode 100644 index 000000000..72e65da34 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart @@ -0,0 +1,175 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/router.dart' as Router; +import 'package:mobx/mobx.dart'; + +class DesktopSidebarWrapper extends BasePage { + final Widget child; + final DesktopSidebarViewModel desktopSidebarViewModel; + final DashboardViewModel dashboardViewModel; + final GlobalKey<NavigatorState> desktopNavigatorKey; + + DesktopSidebarWrapper({ + required this.child, + required this.desktopSidebarViewModel, + required this.dashboardViewModel, + required this.desktopNavigatorKey, + }); + + @override + ObstructingPreferredSizeWidget appBar(BuildContext context) => DesktopDashboardNavbar( + leading: Padding( + padding: EdgeInsets.only(left: sideMenuWidth), + child: getIt<DesktopWalletSelectionDropDown>(), + ), + middle: SyncIndicator( + dashboardViewModel: dashboardViewModel, + onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync), + ), + trailing: InkWell( + onTap: () { + Navigator.of(context).pushNamed( + Routes.unlock, + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { + if (isAuthenticatedSuccessfully) { + auth.close(); + } + }, + ); + }, + child: Icon(Icons.lock_outline), + ), + ); + + @override + bool get resizeToAvoidBottomInset => false; + + final pageController = PageController(); + + final selectedIconPath = 'assets/images/desktop_transactions_solid_icon.png'; + final unselectedIconPath = 'assets/images/desktop_transactions_outline_icon.png'; + + double get sideMenuWidth => 76.0; + + @override + Widget body(BuildContext context) { + _setEffects(); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Observer(builder: (_) { + return SideMenu( + width: sideMenuWidth, + topItems: [ + SideMenuItem( + imagePath: 'assets/images/wallet_outline.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard, + onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.dashboard), + ), + SideMenuItem( + onTap: () { + String? currentPath; + + desktopNavigatorKey.currentState?.popUntil((route) { + currentPath = route.settings.name; + return true; + }); + + switch (currentPath) { + case Routes.transactionsPage: + desktopSidebarViewModel.resetSidebar(); + break; + default: + desktopSidebarViewModel.resetSidebar(); + Future.delayed(Duration(milliseconds: 10), () { + desktopSidebarViewModel.onPageChange(SidebarItem.transactions); + desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage); + }); + } + }, + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions, + imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions + ? selectedIconPath + : unselectedIconPath, + ), + ], + bottomItems: [ + SideMenuItem( + imagePath: 'assets/images/support_icon.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support, + onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)), + SideMenuItem( + imagePath: 'assets/images/settings_outline.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings, + onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings), + ), + ], + ); + }), + Expanded( + child: PageView( + controller: pageController, + physics: NeverScrollableScrollPhysics(), + children: [ + child, + Container( + color: Theme.of(context).backgroundColor, + padding: EdgeInsets.all(20), + child: Navigator( + initialRoute: Routes.support, + onGenerateRoute: (settings) => Router.createRoute(settings), + onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { + return [ + navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! + ]; + }, + ), + ), + Navigator( + initialRoute: Routes.desktop_settings_page, + onGenerateRoute: (settings) => Router.createRoute(settings), + onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { + return [ + navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! + ]; + }, + ), + ], + ), + ), + ], + ); + } + + void _setEffects() async { + reaction<SidebarItem>((_) => desktopSidebarViewModel.currentPage, (page) { + String? currentPath; + + desktopNavigatorKey.currentState?.popUntil((route) { + currentPath = route.settings.name; + return true; + }); + if (page == SidebarItem.transactions) { + return; + } + + if (currentPath == Routes.transactionsPage) { + Navigator.of(desktopNavigatorKey.currentContext!).pop(); + } + pageController.jumpToPage(page.index); + }); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart new file mode 100644 index 000000000..1ad831b1b --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -0,0 +1,199 @@ +import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DesktopWalletSelectionDropDown extends StatefulWidget { + final WalletListViewModel walletListViewModel; + + DesktopWalletSelectionDropDown(this.walletListViewModel, {Key? key}) : super(key: key); + + @override + State<DesktopWalletSelectionDropDown> createState() => _DesktopWalletSelectionDropDownState(); +} + +class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionDropDown> { + final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); + final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); + final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); + + Image _newWalletImage(BuildContext context) => Image.asset( + 'assets/images/new_wallet.png', + height: 12, + width: 12, + color: Theme.of(context).primaryTextTheme.headline6!.color!, + ); + + Image _restoreWalletImage(BuildContext context) => Image.asset( + 'assets/images/restore_wallet.png', + height: 12, + width: 12, + color: Theme.of(context).primaryTextTheme.headline6!.color!, + ); + + Flushbar<void>? _progressBar; + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Observer(builder: (context) { + final dropDownItems = [ + ...widget.walletListViewModel.wallets + .map((wallet) => DesktopDropdownItem( + isSelected: wallet.isCurrent, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: DropDownItemWidget( + title: wallet.name, + image: wallet.isEnabled ? _imageFor(type: wallet.type) : nonWalletTypeIcon), + ), + onSelected: () => _onSelectedWallet(wallet), + )) + .toList(), + DesktopDropdownItem( + onSelected: () => _navigateToCreateWallet(), + child: DropDownItemWidget( + title: S.of(context).create_new, + image: _newWalletImage(context), + ), + ), + DesktopDropdownItem( + onSelected: () => _navigateToRestoreWallet(), + child: DropDownItemWidget( + title: S.of(context).restore_wallet, + image: _restoreWalletImage(context), + ), + ), + ]; + + return DropdownButton<DesktopDropdownItem>( + items: dropDownItems + .map( + (wallet) => DropdownMenuItem<DesktopDropdownItem>( + child: wallet.child, + value: wallet, + ), + ) + .toList(), + onChanged: (item) { + item?.onSelected(); + }, + dropdownColor: themeData.textTheme.bodyText1?.decorationColor, + style: TextStyle(color: themeData.primaryTextTheme.headline6?.color), + selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(), + value: dropDownItems.firstWhere((element) => element.isSelected), + underline: const SizedBox(), + focusColor: Colors.transparent, + borderRadius: BorderRadius.circular(15.0), + ); + }); + } + + void _onSelectedWallet(WalletListItem selectedWallet) async { + if (selectedWallet.isCurrent || !selectedWallet.isEnabled) { + return; + } + final confirmed = await showPopUp<bool>( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).change_wallet_alert_title, + alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).change, + actionLeftButton: () => Navigator.of(context).pop(false), + actionRightButton: () => Navigator.of(context).pop(true)); + }) ?? + false; + + if (confirmed) { + await _loadWallet(selectedWallet); + } + } + + Image _imageFor({required WalletType type}) { + switch (type) { + case WalletType.bitcoin: + return bitcoinIcon; + case WalletType.monero: + return moneroIcon; + case WalletType.litecoin: + return litecoinIcon; + case WalletType.haven: + return havenIcon; + default: + return nonWalletTypeIcon; + } + } + + Future<void> _loadWallet(WalletListItem wallet) async { + if (await widget.walletListViewModel.checkIfAuthRequired()) { + await Navigator.of(context).pushNamed(Routes.auth, + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + if (!isAuthenticatedSuccessfully) { + return; + } + + try { + auth.changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + await widget.walletListViewModel.loadWallet(wallet); + auth.hideProgressText(); + auth.close(); + setState(() {}); + } catch (e) { + auth.changeProcessText( + S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + } + }); + } else { + try { + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + await widget.walletListViewModel.loadWallet(wallet); + hideProgressText(); + setState(() {}); + } catch (e) { + changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + } + } + } + + void _navigateToCreateWallet() { + if (isSingleCoin) { + Navigator.of(context) + .pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } + } + + void _navigateToRestoreWallet() { + if (isSingleCoin) { + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: widget.walletListViewModel.currentWalletType); + } else { + Navigator.of(context).pushNamed(Routes.restoreWalletType); + } + } + + void changeProcessText(String text) { + _progressBar = createBar<void>(text, duration: null)..show(context); + } + + void hideProgressText() { + _progressBar?.dismiss(); + _progressBar = null; + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart b/lib/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart new file mode 100644 index 000000000..47efd04a7 --- /dev/null +++ b/lib/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class DropDownItemWidget extends StatelessWidget { + const DropDownItemWidget({super.key, required this.title, required this.image}); + final double tileHeight = 60; + final Image image; + final String title; + + @override + Widget build(BuildContext context) { + return Container( + height: tileHeight, + padding: EdgeInsets.symmetric(horizontal: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + image, + SizedBox(width: 10), + Flexible( + child: Text( + title, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.headline6!.color!, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart deleted file mode 100644 index 1638c0bc4..000000000 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/screens/dashboard/wallet_menu_item.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -// FIXME: terrible design - -class WalletMenu { - WalletMenu(this.context, this.reconnect, this.hasRescan) : items = [] { - items.addAll([ - WalletMenuItem( - title: S.current.connection_sync, - image: Image.asset('assets/images/nodes_menu.png', - height: 16, width: 16), - handler: () => Navigator.of(context).pushNamed(Routes.connectionSync), - ), - WalletMenuItem( - title: S.current.wallets, - image: Image.asset('assets/images/wallet_menu.png', - height: 16, width: 16), - handler: () => Navigator.of(context).pushNamed(Routes.walletList), - ), - WalletMenuItem( - title: S.current.address_book_menu, - image: Image.asset('assets/images/open_book_menu.png', - height: 16, width: 16), - handler: () => Navigator.of(context).pushNamed(Routes.addressBook), - ), - WalletMenuItem( - title: S.current.security_and_backup, - image: - Image.asset('assets/images/key_menu.png', height: 16, width: 16), - handler: () { - Navigator.of(context).pushNamed(Routes.securityBackupPage); - }), - WalletMenuItem( - title: S.current.privacy_settings, - image: - Image.asset('assets/images/privacy_menu.png', height: 16, width: 16), - handler: () { - Navigator.of(context).pushNamed(Routes.privacyPage); - }), - WalletMenuItem( - title: S.current.display_settings, - image: Image.asset('assets/images/eye_menu.png', - height: 16, width: 16), - handler: () => Navigator.of(context).pushNamed(Routes.displaySettingsPage), - ), - WalletMenuItem( - title: S.current.other_settings, - image: Image.asset('assets/images/settings_menu.png', - height: 16, width: 16), - handler: () => Navigator.of(context).pushNamed(Routes.otherSettingsPage), - ), - WalletMenuItem( - title: S.current.settings_support, - image: Image.asset('assets/images/question_mark.png', - height: 16, width: 16, color: Palette.darkBlue), - handler: () => Navigator.of(context).pushNamed(Routes.support), - ), - ]); - } - - final List<WalletMenuItem> items; - final BuildContext context; - final Future<void> Function() reconnect; - final bool hasRescan; - - void action(int index) { - final item = items[index]; - item.handler(); - } -} diff --git a/lib/src/screens/dashboard/wallet_menu_item.dart b/lib/src/screens/dashboard/wallet_menu_item.dart deleted file mode 100644 index 31a11f31e..000000000 --- a/lib/src/screens/dashboard/wallet_menu_item.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -class WalletMenuItem { - WalletMenuItem({ - required this.title, - required this.image, - required this.handler}); - - final String title; - final Image image; - final void Function() handler; -} \ No newline at end of file diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index 5de8124c4..ebfabcd02 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -24,12 +24,12 @@ import 'package:cake_wallet/di.dart'; class AddressPage extends BasePage { AddressPage({ required this.addressListViewModel, - required this.walletViewModel, + required this.dashboardViewModel, required this.receiveOptionViewModel, }) : _cryptoAmountFocus = FocusNode(); final WalletAddressListViewModel addressListViewModel; - final DashboardViewModel walletViewModel; + final DashboardViewModel dashboardViewModel; final ReceiveOptionViewModel receiveOptionViewModel; final FocusNode _cryptoAmountFocus; @@ -47,28 +47,10 @@ class AddressPage extends BasePage { bool effectsInstalled = false; @override - Widget leading(BuildContext context) { - final _backButton = Icon( - Icons.arrow_back_ios, - color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, - size: 16, - ); + Color get titleColor => Colors.white; - return SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: TextButton( - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), - onPressed: () => onClose(context), - child: _backButton), - ), - ); - } + @override + bool get canUseCloseIcon => true; @override Widget middle(BuildContext context) => @@ -116,8 +98,8 @@ class AddressPage extends BasePage { _setEffects(context); autorun((_) async { - if (!walletViewModel.isOutdatedElectrumWallet || - !walletViewModel.settingsStore.shouldShowReceiveWarning) { + if (!dashboardViewModel.isOutdatedElectrumWallet || + !dashboardViewModel.settingsStore.shouldShowReceiveWarning) { return; } @@ -133,7 +115,7 @@ class AddressPage extends BasePage { actionLeftButton: () => Navigator.of(context).pop(), rightButtonText: S.of(context).do_not_show_me, actionRightButton: () { - walletViewModel.settingsStore.setShouldShowReceiveWarning(false); + dashboardViewModel.settingsStore.setShouldShowReceiveWarning(false); Navigator.of(context).pop(); }); }); @@ -163,7 +145,7 @@ class AddressPage extends BasePage { addressListViewModel: addressListViewModel, amountTextFieldFocusNode: _cryptoAmountFocus, isAmountFieldShow: !addressListViewModel.hasAccounts, - isLight: walletViewModel.settingsStore.currentTheme.type == ThemeType.light)) + isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light)) ), Observer(builder: (_) { return addressListViewModel.hasAddressList @@ -222,7 +204,6 @@ class AddressPage extends BasePage { } reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { - Navigator.pop(context); switch (option) { case ReceivePageOption.anonPayInvoice: Navigator.pushReplacementNamed( diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index 48eaa0a3e..bf8b1ae17 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -1,10 +1,8 @@ -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; 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'; @@ -16,9 +14,6 @@ class BalancePage extends StatelessWidget{ final DashboardViewModel dashboardViewModel; final SettingsStore settingsStore; - - Color get backgroundLightColor => - settingsStore.currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @override Widget build(BuildContext context) { @@ -29,7 +24,7 @@ class BalancePage extends StatelessWidget{ child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 56), + SizedBox(height: ResponsiveLayoutUtil.instance.isMobile(context) ? 56 : 16), Container( margin: const EdgeInsets.only(left: 24, bottom: 16), child: Observer(builder: (_) { diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 87bf956b5..17acdab87 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -1,10 +1,9 @@ -import 'dart:ui'; +import 'package:cake_wallet/src/widgets/setting_action_button.dart'; +import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; // FIXME: terrible design. @@ -19,18 +18,18 @@ class MenuWidget extends StatefulWidget { } class MenuWidgetState extends State<MenuWidget> { - MenuWidgetState() - : this.menuWidth = 0, - this.screenWidth = 0, - this.screenHeight = 0, - this.headerHeight = 120, - this.tileHeight = 60, - this.fromTopEdge = 50, - this.fromBottomEdge = 25, - this.moneroIcon = Image.asset('assets/images/monero_menu.png'), - this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), - this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), - this.havenIcon = Image.asset('assets/images/haven_menu.png'); + MenuWidgetState() + : this.menuWidth = 0, + this.screenWidth = 0, + this.screenHeight = 0, + this.headerHeight = 120, + this.tileHeight = 60, + this.fromTopEdge = 50, + this.fromBottomEdge = 25, + this.moneroIcon = Image.asset('assets/images/monero_menu.png'), + this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), + this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), + this.havenIcon = Image.asset('assets/images/haven_menu.png'); final largeScreen = 731; @@ -82,16 +81,12 @@ class MenuWidgetState extends State<MenuWidget> { @override Widget build(BuildContext context) { - final walletMenu = WalletMenu( - context, - () async => widget.dashboardViewModel.reconnect(), - widget.dashboardViewModel.hasRescan); - final itemCount = walletMenu.items.length; + final itemCount = SettingActions.all.length; moneroIcon = Image.asset('assets/images/monero_menu.png', - color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); + color: Theme.of(context).accentTextTheme.overline!.decorationColor!); bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', - color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); + color: Theme.of(context).accentTextTheme.overline!.decorationColor!); litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); havenIcon = Image.asset('assets/images/haven_menu.png'); @@ -105,17 +100,15 @@ class MenuWidgetState extends State<MenuWidget> { height: 60, width: 4, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(2)), - color: PaletteDark.gray), + borderRadius: BorderRadius.all(Radius.circular(2)), color: PaletteDark.gray), )), SizedBox(width: 12), Expanded( child: ClipRRect( borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - bottomLeft: Radius.circular(24)), + topLeft: Radius.circular(24), bottomLeft: Radius.circular(24)), child: Container( - color: Theme.of(context).textTheme!.bodyText1!.decorationColor!, + color: Theme.of(context).textTheme.bodyText1!.decorationColor!, child: ListView.separated( padding: EdgeInsets.only(top: 0), itemBuilder: (_, index) { @@ -123,25 +116,13 @@ class MenuWidgetState extends State<MenuWidget> { return Container( height: headerHeight, decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context) - .accentTextTheme! - .headline4! - .color!, - Theme.of(context) - .accentTextTheme! - .headline4! - .decorationColor!, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight), + gradient: LinearGradient(colors: [ + Theme.of(context).accentTextTheme.headline4!.color!, + Theme.of(context).accentTextTheme.headline4!.decorationColor!, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), ), padding: EdgeInsets.only( - left: 24, - top: fromTopEdge, - right: 24, - bottom: fromBottomEdge), + left: 24, top: fromTopEdge, right: 24, bottom: fromBottomEdge), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ @@ -150,13 +131,10 @@ class MenuWidgetState extends State<MenuWidget> { SingleChildScrollView( child: Container( child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: - widget.dashboardViewModel.subname != - null - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: widget.dashboardViewModel.subname.isNotEmpty + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, children: <Widget>[ Text( widget.dashboardViewModel.name, @@ -165,19 +143,16 @@ class MenuWidgetState extends State<MenuWidget> { fontSize: 16, fontWeight: FontWeight.bold), ), - if (widget.dashboardViewModel.subname != - null) + if (widget.dashboardViewModel.subname.isNotEmpty) Observer( builder: (_) => Text( - widget.dashboardViewModel - .subname, + widget.dashboardViewModel.subname, style: TextStyle( color: Theme.of(context) - .accentTextTheme! + .accentTextTheme .overline! .decorationColor!, - fontWeight: - FontWeight.w500, + fontWeight: FontWeight.w500, fontSize: 12), )) ], @@ -190,58 +165,24 @@ class MenuWidgetState extends State<MenuWidget> { index--; - final item = walletMenu.items[index]; - final title = item.title; - final image = item.image ?? Offstage(); + final item = SettingActions.all[index]; + final isLastTile = index == itemCount - 1; - return GestureDetector( - onTap: () { - Navigator.of(context).pop(); - walletMenu.action(index); - }, - child: Container( - color: Theme.of(context) - .textTheme! - .bodyText1! - .decorationColor!, - height: isLastTile ? headerHeight : tileHeight, - padding: isLastTile - ? EdgeInsets.only( - left: 24, - right: 24, - top: fromBottomEdge, - //bottom: fromTopEdge - ) - : EdgeInsets.only(left: 24, right: 24), - alignment: isLastTile ? Alignment.topLeft : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - image, - SizedBox(width: 16), - Expanded( - child: Text( - title, - style: TextStyle( - color: Theme.of(context) - .textTheme! - .headline3! - .color!, - fontSize: 16, - fontWeight: FontWeight.bold), - )) - ], - ), - )); + return SettingActionButton( + isLastTile: isLastTile, + tileHeight: tileHeight, + selectionActive: false, + fromBottomEdge: fromBottomEdge, + fromTopEdge: fromTopEdge, + onTap: () => item.onTap.call(context), + image: item.image, + title: item.name, + ); }, separatorBuilder: (_, index) => Container( height: 1, - color: Theme.of(context) - .primaryTextTheme! - .caption! - .decorationColor!, + color: Theme.of(context).primaryTextTheme.caption!.decorationColor!, ), itemCount: itemCount + 1), ))) diff --git a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart index 2ca63dd82..8b3ffb894 100644 --- a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart +++ b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart @@ -86,7 +86,11 @@ class PresentReceiveOptionPicker extends StatelessWidget { itemBuilder: (_, index) { final option = receiveOptionViewModel.options[index]; return InkWell( - onTap: () => receiveOptionViewModel.selectReceiveOption(option), + onTap: () { + Navigator.pop(popUpContext); + + receiveOptionViewModel.selectReceiveOption(option); + }, child: Padding( padding: const EdgeInsets.only(left: 24, right: 24), child: Observer(builder: (_) { diff --git a/lib/src/screens/dashboard/widgets/transactions_page.dart b/lib/src/screens/dashboard/widgets/transactions_page.dart index f773fa8fc..de26d8da6 100644 --- a/lib/src/screens/dashboard/widgets/transactions_page.dart +++ b/lib/src/screens/dashboard/widgets/transactions_page.dart @@ -25,6 +25,7 @@ class TransactionsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + color: Theme.of(context).backgroundColor, padding: EdgeInsets.only(top: 24, bottom: 24), child: Column( children: <Widget>[ diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 1f17d38fd..99eeae7dc 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,13 +1,14 @@ -import 'dart:ui'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; +import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -35,7 +36,14 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class ExchangePage extends BasePage { - ExchangePage(this.exchangeViewModel); + ExchangePage(this.exchangeViewModel) { + depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; + receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; + } final ExchangeViewModel exchangeViewModel; final depositKey = GlobalKey<ExchangeCardState>(); @@ -49,6 +57,20 @@ class ExchangePage extends BasePage { final _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); var _isReactionsSet = false; + final arrowBottomPurple = Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ); + final arrowBottomCakeGreen = Image.asset( + 'assets/images/arrow_bottom_cake_green.png', + color: Colors.white, + height: 8, + ); + + late final String? depositWalletName; + late final String? receiveWalletName; + @override String get title => S.current.exchange; @@ -85,28 +107,11 @@ class ExchangePage extends BasePage { exchangeViewModel.reset(); }); + @override + bool get canUseCloseIcon => true; + @override Widget body(BuildContext context) { - final arrowBottomPurple = Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Colors.white, - height: 8, - ); - final arrowBottomCakeGreen = Image.asset( - 'assets/images/arrow_bottom_cake_green.png', - color: Colors.white, - height: 8, - ); - - final depositWalletName = - exchangeViewModel.depositCurrency == CryptoCurrency.xmr - ? exchangeViewModel.wallet.name - : null; - final receiveWalletName = - exchangeViewModel.receiveCurrency == CryptoCurrency.xmr - ? exchangeViewModel.wallet.name - : null; - WidgetsBinding.instance .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); @@ -137,201 +142,7 @@ class ExchangePage extends BasePage { contentPadding: EdgeInsets.only(bottom: 24), content: Observer(builder: (_) => Column( children: <Widget>[ - Container( - padding: EdgeInsets.only(bottom: 32), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient( - colors: [ - Theme.of(context).primaryTextTheme!.bodyText2!.color!, - Theme.of(context) - .primaryTextTheme! - .bodyText2! - .decorationColor!, - ], - stops: [ - 0.35, - 1.0 - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight), - ), - child: Column( - children: <Widget>[ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient( - colors: [ - Theme.of(context) - .primaryTextTheme! - .subtitle2! - .color!, - Theme.of(context) - .primaryTextTheme! - .subtitle2! - .decorationColor!, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight), - ), - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), - child: Observer( - builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - hasAllAmount: exchangeViewModel.hasAllAmount, - allAmount: exchangeViewModel.hasAllAmount - ? () => exchangeViewModel - .calculateDepositAllAmount() - : null, - amountFocusNode: _depositAmountFocus, - addressFocusNode: _depositAddressFocus, - key: depositKey, - title: S.of(context).you_will_send, - initialCurrency: - exchangeViewModel.depositCurrency, - initialWalletName: depositWalletName ?? '', - initialAddress: - exchangeViewModel.depositCurrency == - exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.depositAddress, - initialIsAmountEditable: true, - initialIsAddressEditable: - exchangeViewModel.isDepositAddressEnabled, - isAmountEstimated: false, - hasRefundAddress: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.depositCurrencies, - onCurrencySelected: (currency) { - // FIXME: need to move it into view model - if (currency == CryptoCurrency.xmr && - exchangeViewModel.wallet.type != - WalletType.monero) { - showPopUp<void>( - context: context, - builder: (dialogContext) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: S - .of(context) - .exchange_incorrect_current_wallet_for_xmr, - buttonText: S.of(context).ok, - buttonAction: () => - Navigator.of(dialogContext) - .pop()); - }); - return; - } - - exchangeViewModel.changeDepositCurrency( - currency: currency); - }, - imageArrow: arrowBottomPurple, - currencyButtonColor: Colors.transparent, - addressButtonsColor: - Theme.of(context).focusColor!, - borderColor: Theme.of(context) - .primaryTextTheme! - .bodyText1! - .color!, - currencyValueValidator: AmountValidator( - currency: exchangeViewModel.depositCurrency), - addressTextFieldValidator: AddressValidator( - type: exchangeViewModel.depositCurrency), - onPushPasteButton: (context) async { - final domain = - exchangeViewModel.depositAddress; - final ticker = exchangeViewModel - .depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress( - context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = - exchangeViewModel.depositAddress; - final ticker = exchangeViewModel - .depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress( - context, domain, ticker); - }, - ), - ), - ), - Padding( - padding: - EdgeInsets.only(top: 29, left: 24, right: 24), - child: Observer( - builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - amountFocusNode: _receiveAmountFocus, - addressFocusNode: _receiveAddressFocus, - key: receiveKey, - title: S.of(context).you_will_get, - initialCurrency: - exchangeViewModel.receiveCurrency, - initialWalletName: receiveWalletName ?? '', - initialAddress: exchangeViewModel - .receiveCurrency == - exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.receiveAddress, - initialIsAmountEditable: exchangeViewModel - .isReceiveAmountEditable, - initialIsAddressEditable: - exchangeViewModel - .isReceiveAddressEnabled, - isAmountEstimated: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: - exchangeViewModel.receiveCurrencies, - onCurrencySelected: (currency) => - exchangeViewModel - .changeReceiveCurrency( - currency: currency), - imageArrow: arrowBottomCakeGreen, - currencyButtonColor: Colors.transparent, - addressButtonsColor: - Theme.of(context).focusColor!, - borderColor: Theme.of(context) - .primaryTextTheme! - .bodyText1! - .decorationColor!, - currencyValueValidator: AmountValidator( - currency: exchangeViewModel.receiveCurrency), - addressTextFieldValidator: - AddressValidator( - type: exchangeViewModel - .receiveCurrency), - onPushPasteButton: (context) async { - final domain = - exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel - .receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress( - context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = - exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel - .receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress( - context, domain, ticker); - }, - )), - ) - ], - ), - ), + _exchangeCardsSection(context), Padding( padding: EdgeInsets.only(top: 12, left: 24), child: Row( @@ -427,50 +238,9 @@ class ExchangePage extends BasePage { return Row( children: <Widget>[ - GestureDetector( - onTap: () => - Navigator.of(context).pushNamed(Routes.exchangeTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: Theme.of(context) - .primaryTextTheme! - .headline3! - .decorationColor!, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 34, - padding: EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(20)), - color: Colors.transparent, - ), - child: templates.length >= 1 - ? Icon( - Icons.add, - color: Theme.of(context) - .primaryTextTheme! - .headline2! - .color!, - ) - : Text( - S.of(context).new_template, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme! - .headline2! - .color!, - ), - ), - ), - ), - ), + AddTemplateButton( + onTap: () => Navigator.of(context).pushNamed(Routes.exchangeTemplate), + currentTemplatesLength: templates.length, ), ListView.builder( scrollDirection: Axis.horizontal, @@ -808,5 +578,124 @@ class ExchangePage extends BasePage { } } - void disposeBestRateSync() => exchangeViewModel.bestRateSync?.cancel(); + void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel(); + + Widget _exchangeCardsSection(BuildContext context) { + final firstExchangeCard = Observer(builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + hasAllAmount: exchangeViewModel.hasAllAmount, + allAmount: exchangeViewModel.hasAllAmount + ? () => exchangeViewModel.calculateDepositAllAmount() + : null, + amountFocusNode: _depositAmountFocus, + addressFocusNode: _depositAddressFocus, + key: depositKey, + title: S.of(context).you_will_send, + initialCurrency: exchangeViewModel.depositCurrency, + initialWalletName: depositWalletName ?? '', + initialAddress: + exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.depositAddress, + initialIsAmountEditable: true, + initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, + isAmountEstimated: false, + hasRefundAddress: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.depositCurrencies, + onCurrencySelected: (currency) { + // FIXME: need to move it into view model + if (currency == CryptoCurrency.xmr && + exchangeViewModel.wallet.type != WalletType.monero) { + showPopUp<void>( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: + S.of(context).exchange_incorrect_current_wallet_for_xmr, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(dialogContext).pop()); + }); + return; + } + + exchangeViewModel.changeDepositCurrency(currency: currency); + }, + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor!, + borderColor: Theme.of(context).primaryTextTheme!.bodyText1!.color!, + currencyValueValidator: + AmountValidator(currency: exchangeViewModel.depositCurrency), + addressTextFieldValidator: + AddressValidator(type: exchangeViewModel.depositCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); + + final secondExchangeCard = Observer(builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + amountFocusNode: _receiveAmountFocus, + addressFocusNode: _receiveAddressFocus, + key: receiveKey, + title: S.of(context).you_will_get, + initialCurrency: exchangeViewModel.receiveCurrency, + initialWalletName: receiveWalletName ?? '', + initialAddress: + exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.receiveAddress, + initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, + initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, + isAmountEstimated: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) => + exchangeViewModel.changeReceiveCurrency(currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor!, + borderColor: + Theme.of(context).primaryTextTheme!.bodyText1!.decorationColor!, + currencyValueValidator: + AmountValidator(currency: exchangeViewModel.receiveCurrency), + addressTextFieldValidator: + AddressValidator(type: exchangeViewModel.receiveCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); + + if (ResponsiveLayoutUtil.instance.isMobile(context)) { + return MobileExchangeCardsSection( + firstExchangeCard: firstExchangeCard, + secondExchangeCard: secondExchangeCard, + ); + } + + return DesktopExchangeCardsSection( + firstExchangeCard: firstExchangeCard, + secondExchangeCard: secondExchangeCard, + ); + } } diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index cacc52498..50faf7eb2 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -1,13 +1,9 @@ -import 'dart:ui'; import 'package:cake_wallet/exchange/exchange_provider.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:keyboard_actions/keyboard_actions_config.dart'; -import 'package:keyboard_actions/keyboard_actions_item.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -78,7 +74,7 @@ class ExchangeTemplatePage extends BasePage { config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardBarColor: - Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!, + Theme.of(context).accentTextTheme.bodyText1!.backgroundColor!, nextFocus: false, actions: [ KeyboardActionsItem( @@ -103,115 +99,114 @@ class ExchangeTemplatePage extends BasePage { ), gradient: LinearGradient( colors: [ - Theme.of(context).primaryTextTheme!.bodyText2!.color!, - Theme.of(context).primaryTextTheme!.bodyText2!.decorationColor!, + Theme.of(context).primaryTextTheme.bodyText2!.color!, + Theme.of(context).primaryTextTheme.bodyText2!.decorationColor!, ], stops: [0.35, 1.0], begin: Alignment.topLeft, end: Alignment.bottomRight), ), - child: Column( - children: <Widget>[ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24) + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Column( + children: <Widget>[ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24) + ), + gradient: LinearGradient( + colors: [ + Theme.of(context) + .primaryTextTheme.subtitle2! + .color!, + Theme.of(context) + .primaryTextTheme.subtitle2! + .decorationColor!, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight), ), - gradient: LinearGradient( - colors: [ - Theme.of(context) - .primaryTextTheme! - .subtitle2! - .color!, - Theme.of(context) - .primaryTextTheme! - .subtitle2! - .decorationColor!, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight), - ), - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), - child: Observer( - builder: (_) => ExchangeCard( - amountFocusNode: _depositAmountFocus, - key: depositKey, - title: S.of(context).you_will_send, - initialCurrency: - exchangeViewModel.depositCurrency, - initialWalletName: depositWalletName ?? '', - initialAddress: exchangeViewModel - .depositCurrency == - exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.depositAddress, - initialIsAmountEditable: true, - initialIsAddressEditable: exchangeViewModel - .isDepositAddressEnabled, - isAmountEstimated: false, - hasRefundAddress: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: CryptoCurrency.all, - onCurrencySelected: (currency) => - exchangeViewModel.changeDepositCurrency( - currency: currency), - imageArrow: arrowBottomPurple, - currencyButtonColor: Colors.transparent, - addressButtonsColor: - Theme.of(context).focusColor!, - borderColor: Theme.of(context) - .primaryTextTheme! - .bodyText1! - .color!, - currencyValueValidator: AmountValidator( - currency: exchangeViewModel.depositCurrency), - //addressTextFieldValidator: AddressValidator( - // type: exchangeViewModel.depositCurrency), - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 29, left: 24, right: 24), - child: Observer( + padding: EdgeInsets.fromLTRB(24, 100, 24, 32), + child: Observer( builder: (_) => ExchangeCard( - amountFocusNode: _receiveAmountFocus, - key: receiveKey, - title: S.of(context).you_will_get, + amountFocusNode: _depositAmountFocus, + key: depositKey, + title: S.of(context).you_will_send, initialCurrency: - exchangeViewModel.receiveCurrency, - initialWalletName: receiveWalletName ?? '', - initialAddress: - exchangeViewModel.receiveCurrency == + exchangeViewModel.depositCurrency, + initialWalletName: depositWalletName ?? '', + initialAddress: exchangeViewModel + .depositCurrency == exchangeViewModel.wallet.currency ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.receiveAddress, - initialIsAmountEditable: - exchangeViewModel.provider is - XMRTOExchangeProvider ? true : false, - initialIsAddressEditable: - exchangeViewModel.isReceiveAddressEnabled, - isAmountEstimated: true, + : exchangeViewModel.depositAddress, + initialIsAmountEditable: true, + initialIsAddressEditable: exchangeViewModel + .isDepositAddressEnabled, + isAmountEstimated: false, + hasRefundAddress: true, isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.receiveCurrencies, + currencies: CryptoCurrency.all, onCurrencySelected: (currency) => - exchangeViewModel.changeReceiveCurrency( + exchangeViewModel.changeDepositCurrency( currency: currency), - imageArrow: arrowBottomCakeGreen, + imageArrow: arrowBottomPurple, currencyButtonColor: Colors.transparent, addressButtonsColor: - Theme.of(context).focusColor!, + Theme.of(context).focusColor, borderColor: Theme.of(context) - .primaryTextTheme! - .bodyText1! - .decorationColor!, + .primaryTextTheme.bodyText1! + .color!, currencyValueValidator: AmountValidator( - currency: exchangeViewModel.receiveCurrency), + currency: exchangeViewModel.depositCurrency), //addressTextFieldValidator: AddressValidator( - // type: exchangeViewModel.receiveCurrency), - )), - ) - ], + // type: exchangeViewModel.depositCurrency), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 29, left: 24, right: 24), + child: Observer( + builder: (_) => ExchangeCard( + amountFocusNode: _receiveAmountFocus, + key: receiveKey, + title: S.of(context).you_will_get, + initialCurrency: + exchangeViewModel.receiveCurrency, + initialWalletName: receiveWalletName ?? '', + initialAddress: + exchangeViewModel.receiveCurrency == + exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.receiveAddress, + initialIsAmountEditable: + exchangeViewModel.provider is + XMRTOExchangeProvider ? true : false, + initialIsAddressEditable: + exchangeViewModel.isReceiveAddressEnabled, + isAmountEstimated: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) => + exchangeViewModel.changeReceiveCurrency( + currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).focusColor, + borderColor: Theme.of(context) + .primaryTextTheme.bodyText1! + .decorationColor!, + currencyValueValidator: AmountValidator( + currency: exchangeViewModel.receiveCurrency), + //addressTextFieldValidator: AddressValidator( + // type: exchangeViewModel.receiveCurrency), + )), + ) + ], + ), ), ), bottomSectionPadding: @@ -230,8 +225,7 @@ class ExchangeTemplatePage extends BasePage { textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context) - .primaryTextTheme! - .headline1! + .primaryTextTheme.headline1! .decorationColor!, fontWeight: FontWeight.w500, fontSize: 12), diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 442ee1b24..5ed9c6f7d 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -1,9 +1,8 @@ -import 'dart:ui'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker_item_widget.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/picker_item.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/currency.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; @@ -68,11 +67,9 @@ class CurrencyPickerState extends State<CurrencyPicker> { @override Widget build(BuildContext context) { return AlertBackground( - child: Stack( - alignment: Alignment.center, - children: [ - Column( + child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ if (widget.title?.isNotEmpty ?? false) Container( @@ -94,10 +91,11 @@ class CurrencyPickerState extends State<CurrencyPicker> { child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(30)), child: Container( - color: Theme.of(context).accentTextTheme!.headline6!.color!, + color: Theme.of(context).accentTextTheme.headline6!.color!, child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.65, + maxWidth: ResponsiveLayoutUtil.kPopupWidth ), child: Column( mainAxisSize: MainAxisSize.min, @@ -133,7 +131,7 @@ class CurrencyPickerState extends State<CurrencyPicker> { ), ), Divider( - color: Theme.of(context).accentTextTheme!.headline6!.backgroundColor!, + color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, height: 1, ), if (widget.selectedAtIndex != -1) @@ -171,8 +169,7 @@ class CurrencyPickerState extends State<CurrencyPicker> { ), ), ), - ], - ), + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), AlertCloseButton(), ], ), diff --git a/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart new file mode 100644 index 000000000..0a97d7bad --- /dev/null +++ b/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class DesktopExchangeCardsSection extends StatelessWidget { + final Widget firstExchangeCard; + final Widget secondExchangeCard; + + const DesktopExchangeCardsSection({ + Key? key, + required this.firstExchangeCard, + required this.secondExchangeCard, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Column( + children: <Widget>[ + Padding( + padding: EdgeInsets.only(top: 55, left: 24, right: 24), + child: firstExchangeCard, + ), + Padding( + padding: EdgeInsets.only(top: 29, left: 24, right: 24), + child: secondExchangeCard, + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 2bacbac3c..0ba112215 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -160,7 +160,7 @@ class ExchangeCardState extends State<ExchangeCard> { final copyImage = Image.asset('assets/images/copy_content.png', height: 16, width: 16, - color: Theme.of(context).primaryTextTheme!.headline3!.color!); + color: Theme.of(context).primaryTextTheme.headline3!.color!); return Container( width: double.infinity, @@ -175,7 +175,7 @@ class ExchangeCardState extends State<ExchangeCard> { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme!.headline5!.color!), + color: Theme.of(context).textTheme.headline5!.color!), ) ], ), @@ -210,7 +210,7 @@ class ExchangeCardState extends State<ExchangeCard> { child: Container( height: 32, decoration: BoxDecoration( - color: widget.addressButtonsColor ?? Theme.of(context).primaryTextTheme!.headline4!.color!, + color: widget.addressButtonsColor ?? Theme.of(context).primaryTextTheme.headline4!.color!, borderRadius: BorderRadius.all(Radius.circular(6))), child: Center( @@ -221,8 +221,7 @@ class ExchangeCardState extends State<ExchangeCard> { fontSize: 12, fontWeight: FontWeight.bold, color: Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!)), ), ), @@ -241,34 +240,36 @@ class ExchangeCardState extends State<ExchangeCard> { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( - child: BaseTextFormField( - focusNode: widget.amountFocusNode, - controller: amountController, - enabled: _isAmountEditable, - textAlign: TextAlign.left, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny( - RegExp('[\\-|\\ ]')) - ], - hintText: '0.0000', - borderColor: Colors.transparent, - //widget.borderColor, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white), - placeholderTextStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .accentTextTheme! - .headline1! - .decorationColor!), - validator: _isAmountEditable - ? widget.currencyValueValidator - : null), + child: FocusTraversalOrder( + order: NumericFocusOrder(1), + child: BaseTextFormField( + focusNode: widget.amountFocusNode, + controller: amountController, + enabled: _isAmountEditable, + textAlign: TextAlign.left, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny( + RegExp('[\\-|\\ ]')) + ], + hintText: '0.0000', + borderColor: Colors.transparent, + //widget.borderColor, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white), + placeholderTextStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .accentTextTheme.headline1! + .decorationColor!), + validator: _isAmountEditable + ? widget.currencyValueValidator + : null), + ), ), if (widget.hasAllAmount) Container( @@ -276,8 +277,7 @@ class ExchangeCardState extends State<ExchangeCard> { width: 32, decoration: BoxDecoration( color: Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .color!, borderRadius: BorderRadius.all(Radius.circular(6))), @@ -290,8 +290,7 @@ class ExchangeCardState extends State<ExchangeCard> { fontSize: 12, fontWeight: FontWeight.bold, color: Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!)), ), ), @@ -302,8 +301,7 @@ class ExchangeCardState extends State<ExchangeCard> { ], )), Divider(height: 1,color: Theme.of(context) - .primaryTextTheme! - .headline5! + .primaryTextTheme.headline5! .decorationColor!), Padding( padding: EdgeInsets.only(top: 5), @@ -321,8 +319,7 @@ class ExchangeCardState extends State<ExchangeCard> { fontSize: 10, height: 1.2, color: Theme.of(context) - .accentTextTheme! - .headline1! + .accentTextTheme.headline1! .decorationColor!), ) : Offstage(), @@ -336,8 +333,7 @@ class ExchangeCardState extends State<ExchangeCard> { fontSize: 10, height: 1.2, color: Theme.of(context) - .accentTextTheme! - .headline1! + .accentTextTheme.headline1! .decorationColor!)) : Offstage(), ])), @@ -351,71 +347,75 @@ class ExchangeCardState extends State<ExchangeCard> { fontSize: 14, fontWeight: FontWeight.w500, color: Theme.of(context) - .accentTextTheme! - .headline1! + .accentTextTheme.headline1! .decorationColor!), )) : Offstage(), _isAddressEditable - ? Padding( - padding: EdgeInsets.only(top: 20), - child: AddressTextField( - focusNode: widget.addressFocusNode, - controller: addressController, - onURIScanned: (uri) { - final paymentRequest = PaymentRequest.fromUri(uri); - addressController.text = paymentRequest.address; - - if (amountController.text.isNotEmpty) { - _showAmountPopup(context, paymentRequest); - return; - } - widget.amountFocusNode?.requestFocus(); - amountController.text = paymentRequest.amount; - }, - placeholder: widget.hasRefundAddress - ? S.of(context).refund_address - : null, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook, - ], - isBorderExist: false, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white), - hintStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .accentTextTheme! - .headline1! - .decorationColor!), - buttonColor: widget.addressButtonsColor, - validator: widget.addressTextFieldValidator, - onPushPasteButton: widget.onPushPasteButton, - onPushAddressBookButton: widget.onPushAddressBookButton, - selectedCurrency: _selectedCurrency + ? FocusTraversalOrder( + order: NumericFocusOrder(2), + child: Padding( + padding: EdgeInsets.only(top: 20), + child: AddressTextField( + focusNode: widget.addressFocusNode, + controller: addressController, + onURIScanned: (uri) { + final paymentRequest = PaymentRequest.fromUri(uri); + addressController.text = paymentRequest.address; + + if (amountController.text.isNotEmpty) { + _showAmountPopup(context, paymentRequest); + return; + } + widget.amountFocusNode?.requestFocus(); + amountController.text = paymentRequest.amount; + }, + placeholder: widget.hasRefundAddress + ? S.of(context).refund_address + : null, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook, + ], + isBorderExist: false, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white), + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .accentTextTheme.headline1! + .decorationColor!), + buttonColor: widget.addressButtonsColor, + validator: widget.addressTextFieldValidator, + onPushPasteButton: widget.onPushPasteButton, + onPushAddressBookButton: widget.onPushAddressBookButton, + selectedCurrency: _selectedCurrency + ), + ), - - ) + ) : Padding( padding: EdgeInsets.only(top: 10), child: Builder( builder: (context) => Stack(children: <Widget>[ - BaseTextFormField( - controller: addressController, - readOnly: true, - borderColor: Colors.transparent, - suffixIcon: - SizedBox(width: _isMoneroWallet ? 80 : 36), - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white), - validator: widget.addressTextFieldValidator), + FocusTraversalOrder( + order: NumericFocusOrder(3), + child: BaseTextFormField( + controller: addressController, + readOnly: true, + borderColor: Colors.transparent, + suffixIcon: + SizedBox(width: _isMoneroWallet ? 80 : 36), + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white), + validator: widget.addressTextFieldValidator), + ), Positioned( top: 2, right: 0, @@ -432,10 +432,8 @@ class ExchangeCardState extends State<ExchangeCard> { child: InkWell( onTap: () async { final contact = - await Navigator.of(context, - rootNavigator: true) - .pushNamed(Routes - .pickerAddressBook); + await Navigator.of(context) + .pushNamed(Routes.pickerAddressBook); if (contact is ContactBase && contact.address != null) { @@ -458,8 +456,7 @@ class ExchangeCardState extends State<ExchangeCard> { child: Image.asset( 'assets/images/open_book.png', color: Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!, )), )), diff --git a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart new file mode 100644 index 000000000..762c36a55 --- /dev/null +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class MobileExchangeCardsSection extends StatelessWidget { + final Widget firstExchangeCard; + final Widget secondExchangeCard; + + const MobileExchangeCardsSection({ + Key? key, + required this.firstExchangeCard, + required this.secondExchangeCard, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 32), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.bodyText2!.color!, + Theme.of(context).primaryTextTheme.bodyText2!.decorationColor!, + ], + stops: [0.35, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + children: <Widget>[ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subtitle2!.color!, + Theme.of(context).primaryTextTheme.subtitle2!.decorationColor!, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.fromLTRB(24, 100, 24, 32), + child: firstExchangeCard, + ), + Padding( + padding: EdgeInsets.only(top: 29, left: 24, right: 24), + child: secondExchangeCard, + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/ionia/auth/ionia_create_account_page.dart b/lib/src/screens/ionia/auth/ionia_create_account_page.dart index d2ced5dae..abdf3501c 100644 --- a/lib/src/screens/ionia/auth/ionia_create_account_page.dart +++ b/lib/src/screens/ionia/auth/ionia_create_account_page.dart @@ -66,6 +66,7 @@ class IoniaCreateAccountPage extends BasePage { validator: EmailValidator(), keyboardType: TextInputType.emailAddress, controller: _emailController, + onSubmit: (_) => _createAccount(), ), ), bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), @@ -77,12 +78,7 @@ class IoniaCreateAccountPage extends BasePage { Observer( builder: (_) => LoadingPrimaryButton( text: S.of(context).create_account, - onPressed: () async { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - await _authViewModel.createUser(_emailController.text); - }, + onPressed: _createAccount, isLoading: _authViewModel.createUserState is IoniaCreateStateLoading, color: Theme.of(context).accentTextTheme!.bodyText1!.color!, textColor: Colors.white, @@ -151,4 +147,11 @@ class IoniaCreateAccountPage extends BasePage { Routes.ioniaVerifyIoniaOtpPage, arguments: [authViewModel.email, false], ); + + void _createAccount() async { + if (_formKey.currentState != null && !_formKey.currentState!.validate()) { + return; + } + await _authViewModel.createUser(_emailController.text); + } } diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/ionia/auth/ionia_login_page.dart index 6dc4aa6e5..e6e8680f3 100644 --- a/lib/src/screens/ionia/auth/ionia_login_page.dart +++ b/lib/src/screens/ionia/auth/ionia_login_page.dart @@ -57,6 +57,7 @@ class IoniaLoginPage extends BasePage { keyboardType: TextInputType.emailAddress, validator: EmailValidator(), controller: _emailController, + onSubmit: (text) => _login(), ), ), bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), @@ -68,12 +69,7 @@ class IoniaLoginPage extends BasePage { Observer( builder: (_) => LoadingPrimaryButton( text: S.of(context).login, - onPressed: () async { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - await _authViewModel.signIn(_emailController.text); - }, + onPressed: _login, isLoading: _authViewModel.signInState is IoniaCreateStateLoading, color: Theme.of(context).accentTextTheme!.bodyText1!.color!, textColor: Colors.white, @@ -106,4 +102,11 @@ class IoniaLoginPage extends BasePage { Routes.ioniaVerifyIoniaOtpPage, arguments: [authViewModel.email, true], ); + + void _login() async { + if (_formKey.currentState != null && !_formKey.currentState!.validate()) { + return; + } + await _authViewModel.signIn(_emailController.text); + } } diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart index 625bd36b0..e2123e164 100644 --- a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart +++ b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart @@ -82,6 +82,7 @@ class IoniaVerifyIoniaOtp extends BasePage { keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), focusNode: _codeFocus, controller: _codeController, + onSubmit: (_) => _verify(), ), SizedBox(height: 14), Text( @@ -116,7 +117,7 @@ class IoniaVerifyIoniaOtp extends BasePage { Observer( builder: (_) => LoadingPrimaryButton( text: S.of(context).continue_text, - onPressed: () async => await _authViewModel.verifyEmail(_codeController.text), + onPressed: _verify, isDisabled: _authViewModel.otpState is IoniaOtpSendDisabled, isLoading: _authViewModel.otpState is IoniaOtpValidating, color: Theme.of(context).accentTextTheme!.bodyText1!.color!, @@ -148,4 +149,6 @@ class IoniaVerifyIoniaOtp extends BasePage { void _onOtpSuccessful(BuildContext context) => Navigator.of(context) .pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); + + void _verify() async => await _authViewModel.verifyEmail(_codeController.text); } 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 3182d366f..a60b967f2 100644 --- a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -6,6 +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/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -44,7 +45,6 @@ class IoniaBuyGiftCardPage extends BasePage { @override Widget body(BuildContext context) { - final _width = MediaQuery.of(context).size.width; final merchant = ioniaBuyCardViewModel.ioniaMerchant; return KeyboardActions( disableScroll: true, @@ -67,7 +67,10 @@ class IoniaBuyGiftCardPage extends BasePage { Container( padding: EdgeInsets.symmetric(horizontal: 25), decoration: BoxDecoration( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), gradient: LinearGradient(colors: [ Theme.of(context).primaryTextTheme!.subtitle1!.color!, Theme.of(context).primaryTextTheme!.subtitle1!.decorationColor!, @@ -75,35 +78,28 @@ class IoniaBuyGiftCardPage extends BasePage { ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(height: 150), - BaseTextFormField( - controller: _amountController, - focusNode: _amountFieldFocus, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), - FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,2}'))], - hintText: '1000', - placeholderTextStyle: TextStyle( - color: Theme.of(context).primaryTextTheme!.headline5!.color!, - fontWeight: FontWeight.w600, - fontSize: 36, - ), - borderColor: Theme.of(context).primaryTextTheme!.headline5!.color!, - textColor: Colors.white, - textStyle: TextStyle( - color: Colors.white, - fontSize: 36, - ), - prefixIcon: Padding( - padding: EdgeInsets.only( - top: 5.0, - left: _width / 4, + SizedBox( + width: 200, + child: BaseTextFormField( + controller: _amountController, + focusNode: _amountFieldFocus, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d+(\.|\,)?\d{0,2}'), + ), + ], + hintText: '1000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline5!.color!, + fontWeight: FontWeight.w600, + fontSize: 36, ), - child: Text( + prefixIcon: Text( 'USD: ', style: TextStyle( color: Colors.white, @@ -111,8 +107,17 @@ class IoniaBuyGiftCardPage extends BasePage { fontSize: 36, ), ), + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + fontSize: 36, + ), ), ), + Divider( + color: Theme.of(context).primaryTextTheme.headline5!.color!, + height: 1, + ), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -140,7 +145,11 @@ class IoniaBuyGiftCardPage extends BasePage { padding: const EdgeInsets.all(24.0), child: CardItem( title: merchant.legalName, - backgroundColor: Theme.of(context).accentTextTheme!.headline1!.backgroundColor!.withOpacity(0.1), + backgroundColor: Theme.of(context) + .accentTextTheme! + .headline1! + .backgroundColor! + .withOpacity(0.1), discount: merchant.discount, titleColor: Theme.of(context).accentTextTheme!.headline1!.backgroundColor!, subtitleColor: Theme.of(context).hintColor, diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 9e0cd898c..0f15e23c5 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,11 +1,12 @@ import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/wallet_name_validator.dart'; import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; @@ -24,18 +25,14 @@ class NewWalletPage extends BasePage { final walletNameImage = Image.asset('assets/images/wallet_name.png'); - final walletNameLightImage = - Image.asset('assets/images/wallet_name_light.png'); + final walletNameLightImage = Image.asset('assets/images/wallet_name_light.png'); @override String get title => S.current.new_wallet; @override Widget body(BuildContext context) => WalletNameForm( - _walletNewVM, - currentTheme.type == ThemeType.dark - ? walletNameImage - : walletNameLightImage); + _walletNewVM, currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage); } class WalletNameForm extends StatefulWidget { @@ -50,9 +47,9 @@ class WalletNameForm extends StatefulWidget { class _WalletNameFormState extends State<WalletNameForm> { _WalletNameFormState(this._walletNewVM) - : _formKey = GlobalKey<FormState>(), - _languageSelectorKey = GlobalKey<SeedLanguageSelectorState>(), - _controller = TextEditingController(); + : _formKey = GlobalKey<FormState>(), + _languageSelectorKey = GlobalKey<SeedLanguageSelectorState>(), + _controller = TextEditingController(); static const aspectRatioImage = 1.22; @@ -64,10 +61,9 @@ class _WalletNameFormState extends State<WalletNameForm> { @override void initState() { - _stateReaction ??= - reaction((_) => _walletNewVM.state, (ExecutionState state) { + _stateReaction ??= reaction((_) => _walletNewVM.state, (ExecutionState state) async { if (state is ExecutedSuccessfullyState) { - Navigator.of(context) + Navigator.of(navigatorKey.currentContext!) .pushNamed(Routes.preSeed, arguments: _walletNewVM.type); } @@ -90,117 +86,118 @@ class _WalletNameFormState extends State<WalletNameForm> { @override Widget build(BuildContext context) { - return Container( + return Padding( padding: EdgeInsets.only(top: 24), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - content: - Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: EdgeInsets.only(left: 12, right: 12), - child: AspectRatio( - aspectRatio: aspectRatioImage, - child: - FittedBox(child: widget.walletImage, fit: BoxFit.fill)), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: Form( - key: _formKey, - child: Stack( - alignment: Alignment.centerRight, - children: [ - TextFormField( - onChanged: (value) => _walletNewVM.name = value, - controller: _controller, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w600, - color: - Theme.of(context).primaryTextTheme!.headline6!.color!), - decoration: InputDecoration( - hintStyle: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .color!), - hintText: S.of(context).wallet_name, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context) - .accentTextTheme! - .headline2! - .decorationColor!, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context) - .accentTextTheme! - .headline2! - .decorationColor!, - width: 1.0), - ), - suffixIcon: IconButton( - onPressed: () async { - final rName = await generateName(); - FocusManager.instance.primaryFocus?.unfocus(); + content: Center( + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(left: 12, right: 12), + child: AspectRatio( + aspectRatio: aspectRatioImage, + child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: Form( + key: _formKey, + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + onChanged: (value) => _walletNewVM.name = value, + controller: _controller, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme!.headline6!.color!), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.color!), + hintText: S.of(context).wallet_name, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .accentTextTheme! + .headline2! + .decorationColor!, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .accentTextTheme! + .headline2! + .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! - .headline4! - .decorationColor!, + 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! + .headline4! + .decorationColor!, + ), + ), + ), ), + validator: WalletNameValidator(), ), - ), + ], ), - validator: WalletNameValidator(), ), - ], - ), + ), + if (_walletNewVM.hasLanguageSelector) ...[ + Padding( + padding: EdgeInsets.only(top: 40), + child: Text( + S.of(context).seed_language_choose, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme!.headline6!.color!), + ), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: SeedLanguageSelector( + key: _languageSelectorKey, initialSelected: defaultSeedLanguage), + ) + ] + ], ), ), - if (_walletNewVM.hasLanguageSelector) ...[ - Padding( - padding: EdgeInsets.only(top: 40), - child: Text( - S.of(context).seed_language_choose, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).primaryTextTheme!.headline6!.color!), - ), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: SeedLanguageSelector( - key: _languageSelectorKey, - initialSelected: defaultSeedLanguage), - ) - ] - ]), - bottomSectionPadding: - EdgeInsets.all(24), + ), + bottomSectionPadding: EdgeInsets.all(24), bottomSection: Column( children: [ Observer( diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 41179f34c..407582923 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; @@ -13,23 +14,19 @@ class NewWalletTypePage extends BasePage { final void Function(BuildContext, WalletType) onTypeSelected; final walletTypeImage = Image.asset('assets/images/wallet_type.png'); - final walletTypeLightImage = - Image.asset('assets/images/wallet_type_light.png'); + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @override String get title => S.current.wallet_list_restore_wallet; @override Widget body(BuildContext context) => WalletTypeForm( - onTypeSelected: onTypeSelected, - walletImage: currentTheme.type == ThemeType.dark - ? walletTypeImage - : walletTypeLightImage); + onTypeSelected: onTypeSelected, + walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage); } class WalletTypeForm extends StatefulWidget { - WalletTypeForm({required this.onTypeSelected, - required this.walletImage}); + WalletTypeForm({required this.onTypeSelected, required this.walletImage}); final void Function(BuildContext, WalletType) onTypeSelected; final Image walletImage; @@ -39,22 +36,16 @@ class WalletTypeForm extends StatefulWidget { } class WalletTypeFormState extends State<WalletTypeForm> { - WalletTypeFormState() - : types = availableWalletTypes; + WalletTypeFormState() : types = availableWalletTypes; static const aspectRatioImage = 1.22; - final moneroIcon = - Image.asset('assets/images/monero_logo.png', height: 24, width: 24); - final bitcoinIcon = - Image.asset('assets/images/bitcoin.png', height: 24, width: 24); - final litecoinIcon = - Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); + final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final walletTypeImage = Image.asset('assets/images/wallet_type.png'); - final walletTypeLightImage = - Image.asset('assets/images/wallet_type_light.png'); - final havenIcon = - Image.asset('assets/images/haven_logo.png', height: 24, width: 24); + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); + final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); WalletType? selected; List<WalletType> types; @@ -69,35 +60,40 @@ class WalletTypeFormState extends State<WalletTypeForm> { Widget build(BuildContext context) { return ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - Padding( - padding: EdgeInsets.only(left: 12, right: 12), - child: AspectRatio( - aspectRatio: aspectRatioImage, - child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)), + content: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + Padding( + padding: EdgeInsets.only(left: 12, right: 12), + child: AspectRatio( + aspectRatio: aspectRatioImage, + child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)), + ), + Padding( + padding: EdgeInsets.only(top: 48), + child: Text( + S.of(context).choose_wallet_currency, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.headline6!.color!), + ), + ), + ...types.map((type) => Padding( + padding: EdgeInsets.only(top: 24), + child: SelectButton( + image: _iconFor(type), + text: walletTypeToDisplayName(type), + isSelected: selected == type, + onTap: () => setState(() => selected = type)), + )) + ], ), - Padding( - padding: EdgeInsets.only(top: 48), - child: Text( - S.of(context).choose_wallet_currency, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).primaryTextTheme.headline6!.color!), - ), - ), - ...types.map((type) => Padding( - padding: EdgeInsets.only(top: 24), - child: SelectButton( - image: _iconFor(type), - text: walletTypeToDisplayName(type), - isSelected: selected == type, - onTap: () => setState(() => selected = type)), - )) - ], + ), ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: PrimaryButton( @@ -121,7 +117,8 @@ class WalletTypeFormState extends State<WalletTypeForm> { case WalletType.haven: return havenIcon; default: - throw Exception('_iconFor: Incorrect Wallet Type. Cannot find icon for Wallet Type: ${type.toString()}'); + throw Exception( + '_iconFor: Incorrect Wallet Type. Cannot find icon for Wallet Type: ${type.toString()}'); } } diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 723d8b1cc..6ca77d8de 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/node.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index a647f3d95..8f30136d0 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -1,17 +1,19 @@ +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:another_flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/services.dart'; class PinCodeWidget extends StatefulWidget { - PinCodeWidget( - {required Key key, - required this.onFullPin, - required this.initialPinLength, - required this.onChangedPin, - required this.hasLengthSwitcher, - this.onChangedPinLength,}) - : super(key: key); + PinCodeWidget({ + required Key key, + required this.onFullPin, + required this.initialPinLength, + required this.onChangedPin, + required this.hasLengthSwitcher, + this.onChangedPinLength, + }) : super(key: key); final void Function(String pin, PinCodeState state) onFullPin; final void Function(String pin) onChangedPin; @@ -25,10 +27,10 @@ class PinCodeWidget extends StatefulWidget { class PinCodeState<T extends PinCodeWidget> extends State<T> { PinCodeState() - : _aspectRatio = 0, - pinLength = 0, - pin = '', - title = ''; + : _aspectRatio = 0, + pinLength = 0, + pin = '', + title = ''; static const defaultPinLength = fourPinLength; static const sixPinLength = 6; static const fourPinLength = 4; @@ -75,8 +77,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> { void setDefaultPinLength() => changePinLength(widget.initialPinLength); void calculateAspectRatio() { - final renderBox = - _gridViewKey.currentContext!.findRenderObject() as RenderBox; + final renderBox = _gridViewKey.currentContext!.findRenderObject() as RenderBox; final cellWidth = renderBox.size.width / 3; final cellHeight = renderBox.size.height / 4; @@ -89,8 +90,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> { void changeProcessText(String text) { hideProgressText(); - _progressBar = createBar<void>(text, duration: null) - ..show(_key.currentContext!); + _progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!); } void close() { @@ -104,8 +104,8 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> { } @override - Widget build(BuildContext context) => Scaffold( - key: _key, body: body(context), resizeToAvoidBottomInset: false); + Widget build(BuildContext context) => + Scaffold(key: _key, body: body(context), resizeToAvoidBottomInset: false); Widget body(BuildContext context) { final deleteIconImage = Image.asset( @@ -117,157 +117,184 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> { color: Theme.of(context).primaryTextTheme!.headline6!.color!, ); - return Container( - color: Theme.of(context).backgroundColor, - padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 40.0), - child: Column(children: <Widget>[ - Spacer(flex: 2), - Text(title, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).primaryTextTheme!.headline6!.color!)), - Spacer(flex: 3), - Container( - width: 180, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(pinLength, (index) { - const size = 10.0; - final isFilled = pin.length > index ? pin[index] != null : false; - - return Container( - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isFilled - ? Theme.of(context).primaryTextTheme!.headline6!.color! - : Theme.of(context) - .accentTextTheme! - .bodyText2! - .color! - .withOpacity(0.25), - )); - }), - ), - ), - Spacer(flex: 2), - if (widget.hasLengthSwitcher) ...[ - TextButton( - onPressed: () { - changePinLength(pinLength == PinCodeState.fourPinLength - ? PinCodeState.sixPinLength - : PinCodeState.fourPinLength); - }, - child: Text( - _changePinLengthText(), + return RawKeyboardListener( + focusNode: FocusNode(), + autofocus: true, + onKey: (keyEvent) { + if (keyEvent is RawKeyDownEvent) { + if (keyEvent.logicalKey.keyLabel == "Backspace") { + _pop(); + return; + } + int? number = int.tryParse(keyEvent.character ?? ''); + if (number != null) { + _push(number); + } + } + }, + child: Container( + color: Theme.of(context).backgroundColor, + padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 40.0), + child: Column( + children: <Widget>[ + Spacer(flex: 2), + Text(title, style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - color: Theme.of(context) - .accentTextTheme! - .bodyText2! - .decorationColor!), - )) - ], - Spacer(flex: 1), - Flexible( - flex: 24, - child: Container( - key: _gridViewKey, - child: _aspectRatio > 0 - ? GridView.count( - shrinkWrap: true, - crossAxisCount: 3, - childAspectRatio: _aspectRatio, - physics: const NeverScrollableScrollPhysics(), - children: List.generate(12, (index) { - const double marginRight = 15; - const double marginLeft = 15; + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme!.headline6!.color!)), + Spacer(flex: 3), + Container( + width: 180, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(pinLength, (index) { + const size = 10.0; + final isFilled = pin.length > index ? pin[index] != null : false; - if (index == 9) { - return Container( - margin: EdgeInsets.only( - left: marginLeft, right: marginRight), - child: TextButton( - onPressed: () => null, - // (widget.hasLengthSwitcher || - // !settingsStore - // .allowBiometricalAuthentication) - // ? null - // : () { - // FIXME -// if (authStore != null) { -// WidgetsBinding.instance.addPostFrameCallback((_) { -// final biometricAuth = BiometricAuth(); -// biometricAuth.isAuthenticated().then( -// (isAuth) { -// if (isAuth) { -// authStore.biometricAuth(); -// _key.currentState.showSnackBar( -// SnackBar( -// content: Text(S.of(context).authenticated), -// backgroundColor: Colors.green, -// ), -// ); -// } -// } -// ); -// }); -// } -// }, - // FIX-ME: Style - //color: Theme.of(context).backgroundColor, - //shape: CircleBorder(), - child: Container() - // (widget.hasLengthSwitcher || - // !settingsStore - // .allowBiometricalAuthentication) - // ? Offstage() - // : faceImage, + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isFilled + ? Theme.of(context).primaryTextTheme!.headline6!.color! + : Theme.of(context) + .accentTextTheme! + .bodyText2! + .color! + .withOpacity(0.25), + )); + }), + ), + ), + Spacer(flex: 2), + if (widget.hasLengthSwitcher) ...[ + TextButton( + onPressed: () { + changePinLength(pinLength == PinCodeState.fourPinLength + ? PinCodeState.sixPinLength + : PinCodeState.fourPinLength); + }, + child: Text( + _changePinLengthText(), + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).accentTextTheme!.bodyText2!.decorationColor!), + ), + ) + ], + Spacer(flex: 1), + Flexible( + flex: 24, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + ), + child: Container( + key: _gridViewKey, + child: _aspectRatio > 0 + ? ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: GridView.count( + shrinkWrap: true, + crossAxisCount: 3, + childAspectRatio: _aspectRatio, + physics: const NeverScrollableScrollPhysics(), + children: List.generate(12, (index) { + const double marginRight = 15; + const double marginLeft = 15; + + if (index == 9) { + return Container( + margin: EdgeInsets.only(left: marginLeft, right: marginRight), + child: TextButton( + onPressed: () => null, + // (widget.hasLengthSwitcher || + // !settingsStore + // .allowBiometricalAuthentication) + // ? null + // : () { + // FIXME + // if (authStore != null) { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // final biometricAuth = BiometricAuth(); + // biometricAuth.isAuthenticated().then( + // (isAuth) { + // if (isAuth) { + // authStore.biometricAuth(); + // _key.currentState.showSnackBar( + // SnackBar( + // content: Text(S.of(context).authenticated), + // backgroundColor: Colors.green, + // ), + // ); + // } + // } + // ); + // }); + // } + // }, + // FIX-ME: Style + //color: Theme.of(context).backgroundColor, + //shape: CircleBorder(), + child: Container() + // (widget.hasLengthSwitcher || + // !settingsStore + // .allowBiometricalAuthentication) + // ? Offstage() + // : faceImage, + ), + ); + } else if (index == 10) { + index = 0; + } else if (index == 11) { + return Container( + margin: EdgeInsets.only(left: marginLeft, right: marginRight), + child: TextButton( + onPressed: () => _pop(), + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).backgroundColor, + shape: CircleBorder(), + ), + child: deleteIconImage, + ), + ); + } else { + index++; + } + + return Container( + margin: EdgeInsets.only(left: marginLeft, right: marginRight), + child: TextButton( + onPressed: () => _push(index), + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).backgroundColor, + shape: CircleBorder(), + ), + child: Text('$index', + style: TextStyle( + fontSize: 30.0, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme! + .headline6! + .color!)), ), - ); - } else if (index == 10) { - index = 0; - } else if (index == 11) { - return Container( - margin: EdgeInsets.only( - left: marginLeft, right: marginRight), - child: TextButton( - onPressed: () => _pop(), - // FIX-ME: Style - //color: Theme.of(context).backgroundColor, - //shape: CircleBorder(), - child: deleteIconImage, - ), - ); - } else { - index++; - } - - return Container( - margin: EdgeInsets.only( - left: marginLeft, right: marginRight), - child: TextButton( - onPressed: () => _push(index), - // FIX-ME: Style - //color: Theme.of(context).backgroundColor, - //shape: CircleBorder(), - child: Text('$index', - style: TextStyle( - fontSize: 30.0, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme! - .headline6! - .color!)), + ); + }), ), - ); - }), - ) - : null)) - ]), + ) + : null, + ), + ), + ), + ) + ], + ), + ), ); } diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index 91c9aaef1..055307d08 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option import 'package:cake_wallet/src/screens/receive/widgets/anonpay_input_form.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:flutter/material.dart'; @@ -85,7 +86,7 @@ class AnonPayInvoicePage extends BasePage { child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24), content: Container( - decoration: BoxDecoration( + decoration: DeviceInfo.instance.isMobile ? BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), gradient: LinearGradient( @@ -96,7 +97,7 @@ class AnonPayInvoicePage extends BasePage { begin: Alignment.topLeft, end: Alignment.bottomRight, ), - ), + ) : null, child: Observer(builder: (_) { return Padding( padding: EdgeInsets.fromLTRB(24, 120, 24, 0), @@ -174,7 +175,6 @@ class AnonPayInvoicePage extends BasePage { } reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { - Navigator.pop(context); switch (option) { case ReceivePageOption.mainnet: Navigator.popAndPushNamed(context, Routes.addressPage); diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 96a19f850..9e68ff0e1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; @@ -35,7 +36,7 @@ class QRWidget extends StatelessWidget { @override Widget build(BuildContext context) { final copyImage = Image.asset('assets/images/copy_address.png', - color: Theme.of(context).textTheme!.subtitle1!.decorationColor!); + color: Theme.of(context).textTheme.subtitle1!.decorationColor!); return Column( mainAxisSize: MainAxisSize.min, @@ -51,23 +52,18 @@ class QRWidget extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!), ), ), Row( children: <Widget>[ Spacer(flex: 3), Observer( - builder: (_) { - return Flexible( - flex: 5, - child: GestureDetector( - onTap: () async { - // Get the current brightness: - final double brightness = await DeviceDisplayBrightness.getBrightness(); - - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(1.0); + builder: (_) => Flexible( + flex: 5, + child: GestureDetector( + onTap: () { + changeBrightnessForRoute(() async { await Navigator.pushNamed( context, Routes.fullscreenQR, @@ -75,31 +71,28 @@ class QRWidget extends StatelessWidget { 'qrData': addressListViewModel.uri.toString(), }, ); - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); - }, - child: Hero( - tag: Key(addressListViewModel.uri.toString()), - child: Center( - child: AspectRatio( - aspectRatio: 1.0, - child: Container( - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - border: Border.all( - width: 3, - color: - Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, - ), + }); + }, + child: Hero( + tag: Key(addressListViewModel.uri.toString()), + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all( + width: 3, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, ), - child: QrImage(data: addressListViewModel.uri.toString(), version: qrVersion), ), + child: QrImage(data: addressListViewModel.uri.toString()), ), ), ), ), - ); - } + ), + ), ), Spacer(flex: 3) ], @@ -176,4 +169,23 @@ class QRWidget extends StatelessWidget { ], ); } + + Future<void> changeBrightnessForRoute(Future<void> Function() navigation) async { + // if not mobile, just navigate + if (!DeviceInfo.instance.isMobile) { + navigation(); + return; + } + + // Get the current brightness: + final brightness = await DeviceDisplayBrightness.getBrightness(); + + // ignore: unawaited_futures + DeviceDisplayBrightness.setBrightness(1.0); + + await navigation(); + + // ignore: unawaited_futures + DeviceDisplayBrightness.setBrightness(brightness); + } } diff --git a/lib/src/screens/restore/restore_from_backup_page.dart b/lib/src/screens/restore/restore_from_backup_page.dart index 1ed2aec4b..16aa3dbef 100644 --- a/lib/src/screens/restore/restore_from_backup_page.dart +++ b/lib/src/screens/restore/restore_from_backup_page.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; @@ -39,43 +40,48 @@ class RestoreFromBackupPage extends BasePage { } }); - return Container( - padding: EdgeInsets.only(bottom: 24, left: 24, right: 24), - child: Column(children: [ - Expanded( - child: Container( - child: Center( - child: TextFormField( - obscureText: true, - enableSuggestions: false, - autocorrect: false, - decoration: InputDecoration( - hintText: S.of(context).enter_backup_password), - keyboardType: TextInputType.visiblePassword, - controller: textEditingController, - style: TextStyle(fontSize: 26, color: Colors.black))), - ), - ), - Container( - child: Row(children: [ - Expanded( - child: PrimaryButton( - onPressed: () => presentFilePicker(), - text: S.of(context).select_backup_file, - color: Colors.grey, - textColor: Colors.white)), - SizedBox(width: 20), - Expanded(child: Observer(builder: (_) { - return LoadingPrimaryButton( - isLoading: - restoreFromBackupViewModel.state is IsExecutingState, - onPressed: () => onImportHandler(context), - text: S.of(context).import, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, - textColor: Colors.white); - })) - ])), - ])); + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Padding( + padding: EdgeInsets.only(bottom: 24, left: 24, right: 24), + child: Column(children: [ + Expanded( + child: Container( + child: Center( + child: TextFormField( + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: InputDecoration( + hintText: S.of(context).enter_backup_password), + keyboardType: TextInputType.visiblePassword, + controller: textEditingController, + style: TextStyle(fontSize: 26, color: Colors.black))), + ), + ), + Container( + child: Row(children: [ + Expanded( + child: PrimaryButton( + onPressed: () => presentFilePicker(), + text: S.of(context).select_backup_file, + color: Colors.grey, + textColor: Colors.white)), + SizedBox(width: 20), + Expanded(child: Observer(builder: (_) { + return LoadingPrimaryButton( + isLoading: + restoreFromBackupViewModel.state is IsExecutingState, + onPressed: () => onImportHandler(context), + text: S.of(context).import, + color: Theme.of(context).accentTextTheme!.bodyText1!.color!, + textColor: Colors.white); + })) + ])), + ])), + ), + ); } Future<void> presentFilePicker() async { diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index a49fd3cc7..a7eb03778 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/cupertino.dart'; @@ -18,31 +19,33 @@ class RestoreOptionsPage extends BasePage { @override Widget body(BuildContext context) { - return Container( - width: double.infinity, - height: double.infinity, - padding: EdgeInsets.all(24), - child: SingleChildScrollView( - child: Column( - children: <Widget>[ - RestoreButton( - onPressed: () => - Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome), - image: imageSeedKeys, - title: S.of(context).restore_title_from_seed_keys, - description: - S.of(context).restore_description_from_seed_keys), - Padding( - padding: EdgeInsets.only(top: 24), - child: RestoreButton( + return Center( + child: Container( + width: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + height: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24), + child: SingleChildScrollView( + child: Column( + children: <Widget>[ + RestoreButton( onPressed: () => - Navigator.pushNamed(context, Routes.restoreFromBackup), - image: imageBackup, - title: S.of(context).restore_title_from_backup, - description: S.of(context).restore_description_from_backup), - ) - ], - ), - )); + Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome), + image: imageSeedKeys, + title: S.of(context).restore_title_from_seed_keys, + description: + S.of(context).restore_description_from_seed_keys), + Padding( + padding: EdgeInsets.only(top: 24), + child: RestoreButton( + onPressed: () => + Navigator.pushNamed(context, Routes.restoreFromBackup), + image: imageBackup, + title: S.of(context).restore_title_from_backup, + description: S.of(context).restore_description_from_backup), + ) + ], + ), + )), + ); } } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index ce9af985e..7288d624b 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -23,10 +24,8 @@ import 'package:cake_wallet/core/seed_validator.dart'; class WalletRestorePage extends BasePage { WalletRestorePage(this.walletRestoreViewModel) - : walletRestoreFromSeedFormKey = - GlobalKey<WalletRestoreFromSeedFormState>(), - walletRestoreFromKeysFormKey = - GlobalKey<WalletRestoreFromKeysFromState>(), + : walletRestoreFromSeedFormKey = GlobalKey<WalletRestoreFromSeedFormState>(), + walletRestoreFromKeysFormKey = GlobalKey<WalletRestoreFromKeysFromState>(), _pages = [], _blockHeightFocusNode = FocusNode(), _controller = PageController(initialPage: 0) { @@ -36,9 +35,8 @@ class WalletRestorePage extends BasePage { _pages.add(WalletRestoreFromSeedForm( displayBlockHeightSelector: walletRestoreViewModel.hasBlockchainHeightLanguageSelector, - displayLanguageSelector: - walletRestoreViewModel.hasSeedLanguageSelector, - type: walletRestoreViewModel.type!, + displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, + type: walletRestoreViewModel.type, key: walletRestoreFromSeedFormKey, blockHeightFocusNode: _blockHeightFocusNode, onHeightOrDateEntered: (value) { @@ -48,26 +46,22 @@ class WalletRestorePage extends BasePage { }, onSeedChange: (String seed) { if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { - final hasHeight = walletRestoreFromSeedFormKey - .currentState!.blockchainHeightKey.currentState!.restoreHeightController - .text - .isNotEmpty; + final hasHeight = walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey + .currentState!.restoreHeightController.text.isNotEmpty; if (hasHeight) { walletRestoreViewModel.isButtonEnabled = _isValidSeed(); } } else { - walletRestoreViewModel.isButtonEnabled = _isValidSeed(); + walletRestoreViewModel.isButtonEnabled = _isValidSeed(); } }, onLanguageChange: (_) { if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { - final hasHeight = walletRestoreFromSeedFormKey - .currentState!.blockchainHeightKey.currentState!.restoreHeightController - .text - .isNotEmpty; + final hasHeight = walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey + .currentState!.restoreHeightController.text.isNotEmpty; if (hasHeight) { - walletRestoreViewModel.isButtonEnabled = _isValidSeed(); + walletRestoreViewModel.isButtonEnabled = _isValidSeed(); } } else { walletRestoreViewModel.isButtonEnabled = _isValidSeed(); @@ -78,8 +72,7 @@ class WalletRestorePage extends BasePage { _pages.add(WalletRestoreFromKeysFrom( key: walletRestoreFromKeysFormKey, walletRestoreViewModel: walletRestoreViewModel, - onHeightOrDateEntered: (value) => - walletRestoreViewModel.isButtonEnabled = value)); + onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value)); break; default: break; @@ -97,8 +90,7 @@ class WalletRestorePage extends BasePage { fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', - color: titleColor ?? - Theme.of(context).primaryTextTheme!.headline6!.color!), + color: titleColor ?? Theme.of(context).primaryTextTheme.headline6!.color!), )); final WalletRestoreViewModel walletRestoreViewModel; @@ -129,134 +121,134 @@ class WalletRestorePage extends BasePage { reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) { walletRestoreViewModel.isButtonEnabled = false; - walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState - !.restoreHeightController.text = ''; - walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState - !.dateController.text = ''; + walletRestoreFromSeedFormKey + .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; + walletRestoreFromSeedFormKey + .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = ''; - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState - !.restoreHeightController.text = ''; - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState - !.dateController.text = ''; + walletRestoreFromKeysFormKey + .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; + walletRestoreFromKeysFormKey + .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = ''; }); return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1! - .backgroundColor!, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _blockHeightFocusNode, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: Container( - height: 0, - color: Theme.of(context).backgroundColor, - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: PageView.builder( - onPageChanged: (page) { - walletRestoreViewModel.mode = - page == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys; - }, - controller: _controller, - itemCount: _pages.length, - itemBuilder: (_, index) => - SingleChildScrollView(child: _pages[index]))), - if (_pages.length > 1) - Padding( - padding: EdgeInsets.only(top: 10), - child: SmoothPageIndicator( - controller: _controller, - count: _pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).hintColor.withOpacity(0.5), - activeDotColor: Theme.of(context).hintColor), - )), - Padding( - padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), - child: Observer( - builder: (context) { - return LoadingPrimaryButton( - onPressed: _confirmForm, - text: S.of(context).restore_recover, - color: - Theme.of(context).accentTextTheme!.subtitle2!.decorationColor!, - textColor: - Theme.of(context).accentTextTheme!.headline5!.decorationColor!, - isLoading: walletRestoreViewModel.state is IsExecutingState, - isDisabled: !walletRestoreViewModel.isButtonEnabled, - ); - }, - )) - ]))); + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyText1!.backgroundColor!, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _blockHeightFocusNode, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ], + ), + child: Container( + height: 0, + color: Theme.of(context).backgroundColor, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: PageView.builder( + onPageChanged: (page) { + walletRestoreViewModel.mode = + page == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys; + }, + controller: _controller, + itemCount: _pages.length, + itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]), + ), + ), + if (_pages.length > 1) + Padding( + padding: EdgeInsets.only(top: 10), + child: SmoothPageIndicator( + controller: _controller, + count: _pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).hintColor.withOpacity(0.5), + activeDotColor: Theme.of(context).hintColor, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), + child: Observer( + builder: (context) { + return LoadingPrimaryButton( + onPressed: _confirmForm, + text: S.of(context).restore_recover, + color: Theme.of(context).accentTextTheme.subtitle2!.decorationColor!, + textColor: Theme.of(context).accentTextTheme.headline5!.decorationColor!, + isLoading: walletRestoreViewModel.state is IsExecutingState, + isDisabled: !walletRestoreViewModel.isButtonEnabled, + ); + }, + ), + ) + ], + ), + ), + ), + ), + ); } bool _isValidSeed() { - final seedWords = walletRestoreFromSeedFormKey - .currentState - !.seedWidgetStateKey - .currentState - !.text - .split(' '); + final seedWords = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text.split(' '); - if ((walletRestoreViewModel.type == WalletType.monero || walletRestoreViewModel.type == WalletType.haven) && + if ((walletRestoreViewModel.type == WalletType.monero || + walletRestoreViewModel.type == WalletType.haven) && seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) { return false; } - + if ((walletRestoreViewModel.type == WalletType.bitcoin || - walletRestoreViewModel.type == WalletType.litecoin) && - (seedWords.length != WalletRestoreViewModelBase.electrumSeedMnemonicLength && - seedWords.length != WalletRestoreViewModelBase.electrumShortSeedMnemonicLength)) { + walletRestoreViewModel.type == WalletType.litecoin) && + (seedWords.length != WalletRestoreViewModelBase.electrumSeedMnemonicLength && + seedWords.length != WalletRestoreViewModelBase.electrumShortSeedMnemonicLength)) { return false; } - final words = walletRestoreFromSeedFormKey - .currentState - !.seedWidgetStateKey - .currentState - !.words - .toSet(); - return seedWords - .toSet() - .difference(words) - .toSet() - .isEmpty; + final words = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); + return seedWords.toSet().difference(words).toSet().isEmpty; } Map<String, dynamic> _credentials() { final credentials = <String, dynamic>{}; if (walletRestoreViewModel.mode == WalletRestoreMode.seed) { - credentials['seed'] = walletRestoreFromSeedFormKey - .currentState!.seedWidgetStateKey.currentState!.text; + credentials['seed'] = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { - credentials['height'] = walletRestoreFromSeedFormKey - .currentState!.blockchainHeightKey.currentState!.height; + credentials['height'] = + walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState!.height; } - credentials['name'] = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text; + credentials['name'] = + walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text; } else { - credentials['address'] = - walletRestoreFromKeysFormKey.currentState!.addressController.text; - credentials['viewKey'] = - walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; - credentials['height'] = walletRestoreFromKeysFormKey - .currentState!.blockchainHeightKey.currentState!.height; - credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; + credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; + credentials['spendKey'] = walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + credentials['height'] = + walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; + credentials['name'] = + walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; } return credentials; @@ -272,10 +264,8 @@ class WalletRestorePage extends BasePage { : walletRestoreFromKeysFormKey.currentState!.formKey; final name = walletRestoreViewModel.mode == WalletRestoreMode.seed - ? walletRestoreFromSeedFormKey - .currentState!.nameTextEditingController.value.text - : walletRestoreFromKeysFormKey - .currentState!.nameTextEditingController.value.text; + ? walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.value.text + : walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.value.text; if (!formKey.currentState!.validate()) { return; @@ -301,5 +291,3 @@ class WalletRestorePage extends BasePage { }); } } - - diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 2526d15f3..fe7ea26a8 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -54,7 +55,9 @@ class RootState extends State<Root> with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); super.initState(); - initUniLinks(); + if (DeviceInfo.instance.isMobile) { + initUniLinks(); + } } @override diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index c4ecbbf9a..86d88c96e 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -33,44 +34,48 @@ class PreSeedPage extends BasePage { return WillPopScope( onWillPop: () async => false, child: Container( + alignment: Alignment.center, padding: EdgeInsets.all(24), - child: Column( - children: [ - Flexible( - flex: 2, - child: AspectRatio( - aspectRatio: 1, - child: FittedBox(child: image, fit: BoxFit.contain))), - Flexible( - flex: 3, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.only(top: 70, left: 16, right: 16), - child: Text( - S - .of(context) - .pre_seed_description(wordsCount.toString()), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context) - .primaryTextTheme! - .caption! - .color!), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Column( + children: [ + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: 1, + child: FittedBox(child: image, fit: BoxFit.contain))), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(top: 70, left: 16, right: 16), + child: Text( + S + .of(context) + .pre_seed_description(wordsCount.toString()), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context) + .primaryTextTheme! + .caption! + .color!), + ), ), - ), - PrimaryButton( - onPressed: () => Navigator.of(context) - .popAndPushNamed(Routes.seed, arguments: true), - text: S.of(context).pre_seed_button_text, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, - textColor: Colors.white) - ], - )) - ], + PrimaryButton( + onPressed: () => Navigator.of(context) + .popAndPushNamed(Routes.seed, arguments: true), + text: S.of(context).pre_seed_button_text, + color: Theme.of(context).accentTextTheme!.bodyText1!.color!, + textColor: Colors.white) + ], + )) + ], + ), ), )); } diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index 64895db36..034ab832c 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/share_util.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; @@ -52,7 +53,7 @@ class WalletSeedPage extends BasePage { @override Widget? leading(BuildContext context) => - isNewWalletCreated ? Offstage() : super.leading(context); + isNewWalletCreated ? null: super.leading(context); @override Widget trailing(BuildContext context) { @@ -85,114 +86,119 @@ class WalletSeedPage extends BasePage { return WillPopScope(onWillPop: () async => false, child: Container( padding: EdgeInsets.all(24), - child: Column( - children: <Widget>[ - Flexible( - flex: 2, - child: AspectRatio( - aspectRatio: 1, - child: FittedBox(child: image, fit: BoxFit.fill))), - Flexible( - flex: 3, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Padding( - padding: EdgeInsets.only(top: 33), - child: Observer(builder: (_) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - Text( - walletSeedViewModel.name, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme! - .headline6! - .color!), - ), - Padding( - padding: - EdgeInsets.only(top: 20, left: 16, right: 16), - child: Text( - walletSeedViewModel.seed, - textAlign: TextAlign.center, + alignment: Alignment.center, + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: Column( + children: <Widget>[ + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: 1, + child: FittedBox(child: image, fit: BoxFit.fill))), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Padding( + padding: EdgeInsets.only(top: 33), + child: Observer(builder: (_) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + Text( + walletSeedViewModel.name, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, + fontSize: 20, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme! - .caption! + .headline6! .color!), ), - ) - ], - ); - }), - ), - Column( - children: <Widget>[ - isNewWalletCreated - ? Padding( - padding: EdgeInsets.only( - bottom: 52, left: 43, right: 43), + Padding( + padding: + EdgeInsets.only(top: 20, left: 16, right: 16), child: Text( - S.of(context).seed_reminder, + walletSeedViewModel.seed, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, + fontSize: 14, fontWeight: FontWeight.normal, color: Theme.of(context) .primaryTextTheme! - .overline! + .caption! .color!), ), ) - : Offstage(), - Row( - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: PrimaryButton( - onPressed: () { - ShareUtil.share( - text: walletSeedViewModel.seed, - context: context, - ); - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white), - )), - Flexible( - child: Container( - padding: EdgeInsets.only(left: 8.0), - child: Builder( - builder: (context) => PrimaryButton( - onPressed: () { - Clipboard.setData(ClipboardData( - text: walletSeedViewModel.seed)); - showBar<void>(context, - S.of(context).copied_to_clipboard); - }, - text: S.of(context).copy, - color: Theme.of(context) - .accentTextTheme! - .bodyText2! - .color!, - textColor: Colors.white)), - )) - ], - ) - ], - ) - ], - )) - ], + ], + ); + }), + ), + Column( + children: <Widget>[ + isNewWalletCreated + ? Padding( + padding: EdgeInsets.only( + bottom: 52, left: 43, right: 43), + child: Text( + S.of(context).seed_reminder, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context) + .primaryTextTheme! + .overline! + .color!), + ), + ) + : Offstage(), + Row( + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + Flexible( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: PrimaryButton( + onPressed: () { + ShareUtil.share( + text: walletSeedViewModel.seed, + context: context, + ); + }, + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white), + )), + Flexible( + child: Container( + padding: EdgeInsets.only(left: 8.0), + child: Builder( + builder: (context) => PrimaryButton( + onPressed: () { + Clipboard.setData(ClipboardData( + text: walletSeedViewModel.seed)); + showBar<void>(context, + S.of(context).copied_to_clipboard); + }, + text: S.of(context).copy, + color: Theme.of(context) + .accentTextTheme! + .bodyText2! + .color!, + textColor: Colors.white)), + )) + ], + ) + ], + ) + ], + )) + ], + ), ))); } } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 881536944..c30c46565 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,10 +1,12 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; +import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -19,7 +21,6 @@ import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:dotted_border/dotted_border.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; @@ -50,9 +51,21 @@ class SendPage extends BasePage { @override bool get extendBodyBehindAppBar => true; + @override + bool get canUseCloseIcon => true; + @override AppBarStyle get appBarStyle => AppBarStyle.transparent; + double _sendCardHeight(BuildContext context) { + final double initialHeight = sendViewModel.isElectrumWallet ? 490 : 465; + + if (!ResponsiveLayoutUtil.instance.isMobile(context)) { + return initialHeight - 66; + } + return initialHeight; + } + @override void onClose(BuildContext context) { sendViewModel.onClose(); @@ -104,178 +117,137 @@ class SendPage extends BasePage { key: _formKey, child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24), - content: Column( - children: <Widget>[ - Container( - height: sendViewModel.isElectrumWallet ? 490 : 465, - child: Observer( - builder: (_) { - return PageView.builder( - scrollDirection: Axis.horizontal, - controller: controller, - itemCount: sendViewModel.outputs.length, - itemBuilder: (context, index) { - final output = sendViewModel.outputs[index]; - - return SendCard( - key: output.key, - output: output, - sendViewModel: sendViewModel, - initialPaymentRequest: initialPaymentRequest, - ); - }); - }, - )), - Padding( - padding: - EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10), - child: Container( - height: 10, - child: Observer( - builder: (_) { - final count = sendViewModel.outputs.length; - - return count > 1 - ? SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .primaryTextTheme! - .headline3! - .backgroundColor!, - activeDotColor: Theme.of(context) - .primaryTextTheme! - .headline2! - .backgroundColor!), - ) - : Offstage(); - }, - ), - ), - ), - if (sendViewModel.hasMultiRecipient) - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Observer( - builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; - - return Row( - children: <Widget>[ - GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: Theme.of(context) - .primaryTextTheme! - .headline2! - .decorationColor!, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 34, - padding: EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(20)), - color: Colors.transparent, - ), - child: templates.length >= 1 - ? Icon( - Icons.add, - color: Theme.of(context) - .primaryTextTheme! - .headline2! - .color!, - ) - : Text( - S.of(context).new_template, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme! - .headline2! - .color!, - ), - ), - ), - ), - ), - ), - ListView.builder( + content: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Column( + children: <Widget>[ + Container( + height: _sendCardHeight(context), + child: Observer( + builder: (_) { + return PageView.builder( scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, + controller: controller, + itemCount: sendViewModel.outputs.length, itemBuilder: (context, index) { - final template = templates[index]; - return TemplateTile( - key: UniqueKey(), - to: template.name, - amount: template.isCurrencySelected ? template.amount : template.amountFiat, - from: template.isCurrencySelected ? template.cryptoCurrency : template.fiatCurrency, - onTap: () async { - final fiatFromTemplate = FiatCurrency.all.singleWhere((element) => element.title == template.fiatCurrency); - final output = _defineCurrentOutput(); - output.address = template.address; - if(template.isCurrencySelected){ - output.setCryptoAmount(template.amount); - }else{ - sendViewModel.setFiatCurrency(fiatFromTemplate); - output.setFiatAmount(template.amountFiat); - } - output.resetParsedAddress(); - await output.fetchParsedAddress(context); - }, - onRemove: () { - showPopUp<void>( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: S - .of(context) - .confirm_delete_template, - rightButtonText: S.of(context).delete, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - Navigator.of(dialogContext).pop(); - sendViewModel.sendTemplateViewModel - .removeTemplate( - template: template); - }, - actionLeftButton: () => - Navigator.of(dialogContext) - .pop()); - }, - ); - }, + final output = sendViewModel.outputs[index]; + + return SendCard( + key: output.key, + output: output, + sendViewModel: sendViewModel, + initialPaymentRequest: initialPaymentRequest, ); - }, - ), - ], - ); - }, + }); + }, + )), + Padding( + padding: + EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10), + child: Container( + height: 10, + child: Observer( + builder: (_) { + final count = sendViewModel.outputs.length; + + return count > 1 + ? SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .primaryTextTheme.headline3! + .backgroundColor!, + activeDotColor: Theme.of(context) + .primaryTextTheme.headline2! + .backgroundColor!), + ) + : Offstage(); + }, + ), ), ), - ) - ], + if (sendViewModel.hasMultiRecipient) + Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Observer( + builder: (_) { + final templates = sendViewModel.templates; + final itemCount = templates.length; + + return Row( + children: <Widget>[ + AddTemplateButton( + onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), + currentTemplatesLength: templates.length, + ), + ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + return TemplateTile( + key: UniqueKey(), + to: template.name, + amount: template.isCurrencySelected ? template.amount : template.amountFiat, + from: template.isCurrencySelected ? template.cryptoCurrency : template.fiatCurrency, + onTap: () async { + final fiatFromTemplate = FiatCurrency.all.singleWhere((element) => element.title == template.fiatCurrency); + final output = _defineCurrentOutput(); + output.address = template.address; + if(template.isCurrencySelected){ + output.setCryptoAmount(template.amount); + }else{ + sendViewModel.setFiatCurrency(fiatFromTemplate); + output.setFiatAmount(template.amountFiat); + } + output.resetParsedAddress(); + await output.fetchParsedAddress(context); + }, + onRemove: () { + showPopUp<void>( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).template, + alertContent: S + .of(context) + .confirm_delete_template, + rightButtonText: S.of(context).delete, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + Navigator.of(dialogContext).pop(); + sendViewModel.sendTemplateViewModel + .removeTemplate( + template: template); + }, + actionLeftButton: () => + Navigator.of(dialogContext) + .pop()); + }, + ); + }, + ); + }, + ), + ], + ); + }, + ), + ), + ) + ], + ), ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), @@ -290,8 +262,7 @@ class SendPage extends BasePage { text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme! - .headline3! + .accentTextTheme.headline3! .decorationColor!, ) ) @@ -309,13 +280,11 @@ class SendPage extends BasePage { text: S.of(context).add_receiver, color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme! - .headline3! + .accentTextTheme.headline3! .decorationColor!, isDottedBorder: true, borderColor: Theme.of(context) - .primaryTextTheme! - .headline3! + .primaryTextTheme.headline3! .decorationColor!, )), Observer( @@ -335,7 +304,7 @@ class SendPage extends BasePage { item.address.isEmpty || item.cryptoAmount.isEmpty) .toList(); - if (notValidItems?.isNotEmpty ?? false) { + if (notValidItems.isNotEmpty ?? false) { showErrorValidationAlert(context); return; } @@ -344,7 +313,7 @@ class SendPage extends BasePage { }, text: S.of(context).send, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, + color: Theme.of(context).accentTextTheme.bodyText1!.color!, textColor: Colors.white, isLoading: sendViewModel.state is IsExecutingState || sendViewModel.state is TransactionCommitting, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 082067d95..cdae9a8df 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -119,7 +120,7 @@ class SendCardState extends State<SendCard> color: Colors.transparent, )), Container( - decoration: BoxDecoration( + decoration: ResponsiveLayoutUtil.instance.isMobile(context) ? BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), @@ -130,9 +131,14 @@ class SendCardState extends State<SendCard> .subtitle1! .decorationColor!, ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), + ) : null, child: Padding( - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), + padding: EdgeInsets.fromLTRB( + 24, + ResponsiveLayoutUtil.instance.isMobile(context) ? 100 : 55, + 24, + ResponsiveLayoutUtil.instance.isMobile(context) ? 32 : 0, + ), child: SingleChildScrollView( child: Observer(builder: (_) => Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart new file mode 100644 index 000000000..eee54eb45 --- /dev/null +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -0,0 +1,105 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/setting_action_button.dart'; +import 'package:cake_wallet/src/widgets/setting_actions.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/router.dart' as Router; + +final _settingsNavigatorKey = GlobalKey<NavigatorState>(); + +class DesktopSettingsPage extends StatefulWidget { + const DesktopSettingsPage({super.key}); + + @override + State<DesktopSettingsPage> createState() => _DesktopSettingsPageState(); +} + +class _DesktopSettingsPageState extends State<DesktopSettingsPage> { + final int itemCount = SettingActions.desktopSettings.length; + + int? currentPage; + + void _onItemChange(int index) { + setState(() { + currentPage = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Text( + S.current.settings, + style: textXLarge(), + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 1, + child: ListView.separated( + padding: EdgeInsets.only(top: 0), + itemBuilder: (_, index) { + final item = SettingActions.desktopSettings[index]; + final isLastTile = index == itemCount - 1; + return SettingActionButton( + isLastTile: isLastTile, + selectionActive: currentPage != null, + isSelected: currentPage == index, + isArrowVisible: true, + onTap: () { + if (currentPage != index) { + final settingContext = + _settingsNavigatorKey.currentState?.context ?? context; + item.onTap.call(settingContext); + _onItemChange(index); + } + }, + image: item.image, + title: item.name, + ); + }, + separatorBuilder: (_, index) => Container( + height: 1, + color: Theme.of(context).primaryTextTheme.caption!.decorationColor!, + ), + itemCount: itemCount, + ), + ), + Flexible( + flex: 2, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: Navigator( + key: _settingsNavigatorKey, + initialRoute: Routes.empty_no_route, + onGenerateRoute: (settings) => Router.createRoute(settings), + onGenerateInitialRoutes: + (NavigatorState navigator, String initialRouteName) { + return [ + navigator + .widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! + ]; + }, + ), + ), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index 39123a7eb..4f932b189 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -7,9 +7,9 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.da import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -65,14 +65,15 @@ class DisplaySettingsPage extends BasePage { return LanguageService.list[code]?.toLowerCase().contains(searchText) ?? false; }, ), - SettingsChoicesCell( - ChoicesListItem<ThemeBase>( - title: S.current.color_theme, - items: ThemeList.all, - selectedItem: _displaySettingsViewModel.theme, - onItemSelected: (ThemeBase theme) => _displaySettingsViewModel.setTheme(theme), + if (DeviceInfo.instance.isMobile) + SettingsChoicesCell( + ChoicesListItem<ThemeBase>( + title: S.current.color_theme, + items: ThemeList.all, + selectedItem: _displaySettingsViewModel.theme, + onItemSelected: (ThemeBase theme) => _displaySettingsViewModel.setTheme(theme), + ), ), - ), ], ), ); diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 5edb723bc..801f81775 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_link_provider_cell.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/settings/link_list_item.dart'; import 'package:cake_wallet/view_model/settings/regular_list_item.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; @@ -18,32 +19,34 @@ class SupportPage extends BasePage { @override Widget body(BuildContext context) { - final iconColor = - Theme.of(context).accentTextTheme!.headline1!.backgroundColor!; + final iconColor = Theme.of(context).accentTextTheme!.headline1!.backgroundColor!; // FIX-ME: Added `context` it was not used here before, maby bug ? - return SectionStandardList( - context: context, - sectionCount: 1, - itemCounter: (int _) => supportViewModel.items.length, - itemBuilder: (_, __, index) { - final item = supportViewModel.items[index]; + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: SectionStandardList( + context: context, + sectionCount: 1, + itemCounter: (int _) => supportViewModel.items.length, + itemBuilder: (_, __, index) { + final item = supportViewModel.items[index]; - if (item is RegularListItem) { - return SettingsCellWithArrow( - title: item.title, handler: item.handler); - } + if (item is RegularListItem) { + return SettingsCellWithArrow(title: item.title, handler: item.handler); + } - if (item is LinkListItem) { - return SettingsLinkProviderCell( - title: item.title, - icon: item.icon, - iconColor: item.hasIconColor ? iconColor : null, - link: item.link, - linkTitle: item.linkTitle); - } + if (item is LinkListItem) { + return SettingsLinkProviderCell( + title: item.title, + icon: item.icon, + iconColor: item.hasIconColor ? iconColor : null, + link: item.link, + linkTitle: item.linkTitle); + } - return Container(); - }); + return Container(); + }), + ), + ); } - -} \ No newline at end of file +} diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index d76631b3f..a1e8c51b7 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; @@ -23,8 +24,7 @@ class WalletListPage extends BasePage { final WalletListViewModel walletListViewModel; @override - Widget body(BuildContext context) => - WalletListBody(walletListViewModel: walletListViewModel); + Widget body(BuildContext context) => WalletListBody(walletListViewModel: walletListViewModel); } class WalletListBody extends StatefulWidget { @@ -37,28 +37,21 @@ class WalletListBody extends StatefulWidget { } class WalletListBodyState extends State<WalletListBody> { - final moneroIcon = - Image.asset('assets/images/monero_logo.png', height: 24, width: 24); - final bitcoinIcon = - Image.asset('assets/images/bitcoin.png', height: 24, width: 24); - final litecoinIcon = - Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); - final nonWalletTypeIcon = - Image.asset('assets/images/close.png', height: 24, width: 24); - final havenIcon = - Image.asset('assets/images/haven_logo.png', height: 24, width: 24); + final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); + final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); + final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar<void>? _progressBar; @override Widget build(BuildContext context) { - final newWalletImage = Image.asset('assets/images/new_wallet.png', - height: 12, width: 12, color: Colors.white); + final newWalletImage = + Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', - height: 12, - width: 12, - color: Theme.of(context).primaryTextTheme!.headline6!.color!); + height: 12, width: 12, color: Theme.of(context).primaryTextTheme.headline6!.color!); return Container( padding: EdgeInsets.only(top: 16), @@ -69,16 +62,13 @@ class WalletListBodyState extends State<WalletListBody> { builder: (_) => ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (_, index) => Divider( - color: Theme.of(context).backgroundColor, height: 32), + separatorBuilder: (_, index) => + Divider(color: Theme.of(context).backgroundColor, height: 32), itemCount: widget.walletListViewModel.wallets.length, itemBuilder: (__, index) { final wallet = widget.walletListViewModel.wallets[index]; final currentColor = wallet.isCurrent - ? Theme.of(context) - .accentTextTheme! - .subtitle2! - .decorationColor! + ? Theme.of(context).accentTextTheme.subtitle2!.decorationColor! : Theme.of(context).backgroundColor; final row = GestureDetector( onTap: () async { @@ -90,19 +80,15 @@ class WalletListBodyState extends State<WalletListBody> { context: context, builder: (dialogContext) { return AlertWithTwoActions( - alertTitle: S - .of(context) - .change_wallet_alert_title, - alertContent: S - .of(context) - .change_wallet_alert_content( - wallet.name), + alertTitle: S.of(context).change_wallet_alert_title, + alertContent: + S.of(context).change_wallet_alert_content(wallet.name), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).change, actionLeftButton: () => - Navigator.of(context).pop(false), + Navigator.of(dialogContext).pop(false), actionRightButton: () => - Navigator.of(context).pop(true)); + Navigator.of(dialogContext).pop(true)); }) ?? false; @@ -131,12 +117,11 @@ class WalletListBodyState extends State<WalletListBody> { color: Theme.of(context).backgroundColor, alignment: Alignment.centerLeft, child: Row( - crossAxisAlignment: - CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ wallet.isEnabled - ? _imageFor(type: wallet.type) - : nonWalletTypeIcon, + ? _imageFor(type: wallet.type) + : nonWalletTypeIcon, SizedBox(width: 10), Text( wallet.name, @@ -144,8 +129,7 @@ class WalletListBodyState extends State<WalletListBody> { fontSize: 22, fontWeight: FontWeight.w500, color: Theme.of(context) - .primaryTextTheme! - .headline6! + .primaryTextTheme.headline6! .color!), ) ], @@ -163,43 +147,40 @@ class WalletListBodyState extends State<WalletListBody> { startActionPane: _actionPane(wallet), endActionPane: _actionPane(wallet), child: row, - ); + ); }), ), ), - bottomSectionPadding: - EdgeInsets.only(bottom: 24, right: 24, left: 24), + bottomSectionPadding: EdgeInsets.only(bottom: 24, right: 24, left: 24), bottomSection: Column(children: <Widget>[ PrimaryImageButton( onPressed: () { - if (isSingleCoin) { - Navigator.of(context).pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType); - } else { - Navigator.of(context).pushNamed(Routes.newWalletType); - } - }, + if (isSingleCoin) { + Navigator.of(context).pushNamed(Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } + }, image: newWalletImage, text: S.of(context).wallet_list_create_new_wallet, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, + color: Theme.of(context).accentTextTheme.bodyText1!.color!, textColor: Colors.white, ), SizedBox(height: 10.0), PrimaryImageButton( onPressed: () { - if (isSingleCoin) { - Navigator - .of(context) - .pushNamed( - Routes.restoreWallet, - arguments: widget.walletListViewModel.currentWalletType); - } else { - Navigator.of(context).pushNamed(Routes.restoreWalletType); - } - }, + if (isSingleCoin) { + Navigator.of(context).pushNamed(Routes.restoreWallet, + arguments: widget.walletListViewModel.currentWalletType); + } else { + Navigator.of(context).pushNamed(Routes.restoreWalletType); + } + }, image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).accentTextTheme!.caption!.color!, - textColor: Theme.of(context).primaryTextTheme!.headline6!.color!) + color: Theme.of(context).accentTextTheme.caption!.color!, + textColor: Theme.of(context).primaryTextTheme.headline6!.color!) ])), ); } @@ -232,9 +213,13 @@ class WalletListBodyState extends State<WalletListBody> { await widget.walletListViewModel.loadWallet(wallet); auth.hideProgressText(); auth.close(); - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pop(); - }); + // only pop the wallets route in mobile as it will go back to dashboard page + // in desktop platforms the navigation tree is different + if (DeviceInfo.instance.isMobile) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); + }); + } } catch (e) { auth.changeProcessText( S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); @@ -245,7 +230,11 @@ class WalletListBodyState extends State<WalletListBody> { changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); await widget.walletListViewModel.loadWallet(wallet); hideProgressText(); - Navigator.of(context).pop(); + // only pop the wallets route in mobile as it will go back to dashboard page + // in desktop platforms the navigation tree is different + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pop(); + } } catch (e) { changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); } @@ -290,6 +279,7 @@ class WalletListBodyState extends State<WalletListBody> { ? auth.changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name)) : changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name)); await widget.walletListViewModel.remove(wallet); + hideProgressText(); } catch (e) { auth != null ? auth.changeProcessText( @@ -309,8 +299,10 @@ class WalletListBodyState extends State<WalletListBody> { } void hideProgressText() { - _progressBar?.dismiss(); - _progressBar = null; + Future.delayed(Duration(milliseconds: 50), () { + _progressBar?.dismiss(); + _progressBar = null; + }); } ActionPane _actionPane(WalletListItem wallet) => ActionPane( diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index a50d8ba8c..86e1cbcf1 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -19,7 +20,7 @@ class WelcomePage extends BasePage { if (isHaven) { return S.of(context).haven_app; } - + return S.of(context).cake_wallet; } @@ -31,172 +32,137 @@ class WelcomePage extends BasePage { if (isHaven) { return S.of(context).haven_app_wallet_text; } - + return S.of(context).first_wallet_text; } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Theme - .of(context) - .backgroundColor, + backgroundColor: Theme.of(context).backgroundColor, resizeToAvoidBottomInset: false, body: body(context)); } @override Widget body(BuildContext context) { - final welcomeImage = currentTheme.type == ThemeType.dark - ? welcomeImageDark : welcomeImageLight; + final welcomeImage = currentTheme.type == ThemeType.dark ? welcomeImageDark : welcomeImageLight; final newWalletImage = Image.asset('assets/images/new_wallet.png', height: 12, width: 12, - color: Theme - .of(context) - .accentTextTheme! - .headline5! - .decorationColor!); + color: Theme.of(context).accentTextTheme!.headline5!.decorationColor!); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', - height: 12, - width: 12, - color: Theme.of(context) - .primaryTextTheme! - .headline6! - .color!); + height: 12, width: 12, color: Theme.of(context).primaryTextTheme!.headline6!.color!); - return WillPopScope(onWillPop: () async => false, child: Container( - padding: EdgeInsets.only(top: 64, bottom: 24, left: 24, right: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Flexible( - flex: 2, - child: AspectRatio( - aspectRatio: aspectRatioImage, - child: FittedBox(child: welcomeImage, fit: BoxFit.fill) - ) - ), - Flexible( - flex: 3, + return WillPopScope( + onWillPop: () async => false, + child: Container( + padding: EdgeInsets.only(top: 64, bottom: 24, left: 24, right: 24), + child: Center( + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ - Column( - children: <Widget>[ - Padding( - padding: EdgeInsets.only(top: 24), - child: Text( - S - .of(context) - .welcome, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme - .of(context) - .accentTextTheme! - .headline2! - .color!, + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: aspectRatioImage, + child: FittedBox(child: welcomeImage, fit: BoxFit.fill))), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Column( + children: <Widget>[ + Padding( + padding: EdgeInsets.only(top: 24), + child: Text( + S.of(context).welcome, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.color!, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + appTitle(context), + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryTextTheme!.headline6!.color!, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + appDescription(context), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.color!, + ), + textAlign: TextAlign.center, + ), + ), + ], ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Text( - appTitle(context), - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .primaryTextTheme! - .headline6! - .color!, - ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Text( - appDescription(context), - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme - .of(context) - .accentTextTheme! - .headline2! - .color!, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - Column( - children: <Widget>[ - Text( - S - .of(context) - .please_make_selection, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .color!, - ), - textAlign: TextAlign.center, - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: PrimaryImageButton( - onPressed: () => - Navigator.pushNamed(context, - Routes.newWalletFromWelcome), - image: newWalletImage, - text: S.of(context).create_new, - color: Theme.of(context) - .accentTextTheme! - .subtitle2! - .decorationColor!, - textColor: Theme - .of(context) - .accentTextTheme! - .headline5! - .decorationColor!, - ), - ), - Padding( - padding: EdgeInsets.only(top: 10), - child: PrimaryImageButton( - onPressed: () { - Navigator.pushNamed(context, Routes.restoreOptions); - }, - image: restoreWalletImage, - text: S - .of(context) - .restore_wallet, - color: Theme.of(context) - .accentTextTheme! - .caption! - .color!, - textColor: Theme.of(context) - .primaryTextTheme! - .headline6! - .color!), - ) - ], - ) + Column( + children: <Widget>[ + Text( + S.of(context).please_make_selection, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context).accentTextTheme!.headline2!.color!, + ), + textAlign: TextAlign.center, + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: PrimaryImageButton( + onPressed: () => + Navigator.pushNamed(context, Routes.newWalletFromWelcome), + image: newWalletImage, + text: S.of(context).create_new, + color: Theme.of(context) + .accentTextTheme! + .subtitle2! + .decorationColor!, + textColor: Theme.of(context) + .accentTextTheme! + .headline5! + .decorationColor!, + ), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: PrimaryImageButton( + onPressed: () { + Navigator.pushNamed(context, Routes.restoreOptions); + }, + image: restoreWalletImage, + text: S.of(context).restore_wallet, + color: Theme.of(context).accentTextTheme!.caption!.color!, + textColor: + Theme.of(context).primaryTextTheme!.headline6!.color!), + ) + ], + ) + ], + )) ], - ) - ) - ], - ) - )); + ), + ), + ))); } } diff --git a/lib/src/widgets/add_template_button.dart b/lib/src/widgets/add_template_button.dart new file mode 100644 index 000000000..249c493a6 --- /dev/null +++ b/lib/src/widgets/add_template_button.dart @@ -0,0 +1,52 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; + +class AddTemplateButton extends StatelessWidget { + final Function() onTap; + final int currentTemplatesLength; + + const AddTemplateButton({Key? key, required this.onTap, required this.currentTemplatesLength}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.only(left: 1, right: 10), + child: DottedBorder( + borderType: BorderType.RRect, + dashPattern: [6, 4], + color: Theme.of(context).primaryTextTheme.headline3!.decorationColor!, + strokeWidth: 2, + radius: Radius.circular(20), + child: Container( + height: 34, + padding: EdgeInsets.symmetric( + horizontal: ResponsiveLayoutUtil.instance.isMobile(context) ? 10 : 30), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20)), + color: Colors.transparent, + ), + child: currentTemplatesLength >= 1 + ? Icon( + Icons.add, + color: Theme.of(context).primaryTextTheme.headline2!.color!, + ) + : Text( + S.of(context).new_template, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.headline2!.color!, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 4acc9a145..059dc51aa 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:cake_wallet/utils/device_info.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -65,7 +68,7 @@ class AddressTextField extends StatelessWidget { style: textStyle ?? TextStyle( fontSize: 16, - color: Theme.of(context).primaryTextTheme!.headline6!.color!), + color: Theme.of(context).primaryTextTheme.headline6!.color!), decoration: InputDecoration( suffixIcon: SizedBox( width: prefixIconWidth * options.length + @@ -102,7 +105,8 @@ class AddressTextField extends StatelessWidget { width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: DeviceInfo.instance.isMobile + ? MainAxisAlignment.spaceBetween : MainAxisAlignment.end, children: [ SizedBox(width: 5), if (this.options.contains(AddressTextFieldOption.paste)) ...[ @@ -117,8 +121,7 @@ class AddressTextField extends StatelessWidget { decoration: BoxDecoration( color: buttonColor ?? Theme.of(context) - .accentTextTheme! - .headline6! + .accentTextTheme.headline6! .color!, borderRadius: BorderRadius.all(Radius.circular(6))), @@ -126,13 +129,13 @@ class AddressTextField extends StatelessWidget { 'assets/images/paste_ios.png', color: iconColor ?? Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!, )), )), ], - if (this.options.contains(AddressTextFieldOption.qrCode)) ...[ + if (this.options.contains(AddressTextFieldOption.qrCode) && DeviceInfo.instance.isMobile) + ...[ Container( width: prefixIconWidth, height: prefixIconHeight, @@ -144,8 +147,7 @@ class AddressTextField extends StatelessWidget { decoration: BoxDecoration( color: buttonColor ?? Theme.of(context) - .accentTextTheme! - .headline6! + .accentTextTheme.headline6! .color!, borderRadius: BorderRadius.all(Radius.circular(6))), @@ -153,12 +155,11 @@ class AddressTextField extends StatelessWidget { 'assets/images/qr_code_icon.png', color: iconColor ?? Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!, )), )) - ], + ] else SizedBox(width: 5), if (this .options .contains(AddressTextFieldOption.addressBook)) ...[ @@ -173,8 +174,7 @@ class AddressTextField extends StatelessWidget { decoration: BoxDecoration( color: buttonColor ?? Theme.of(context) - .accentTextTheme! - .headline6! + .accentTextTheme.headline6! .color!, borderRadius: BorderRadius.all(Radius.circular(6))), @@ -182,8 +182,7 @@ class AddressTextField extends StatelessWidget { 'assets/images/open_book.png', color: iconColor ?? Theme.of(context) - .primaryTextTheme! - .headline4! + .primaryTextTheme.headline4! .decorationColor!, )), )) @@ -211,7 +210,7 @@ class AddressTextField extends StatelessWidget { } Future<void> _presetAddressBookPicker(BuildContext context) async { - final contact = await Navigator.of(context, rootNavigator: true) + final contact = await Navigator.of(context) .pushNamed(Routes.pickerAddressBook,arguments: selectedCurrency); if (contact is ContactBase && contact.address != null) { diff --git a/lib/src/widgets/alert_background.dart b/lib/src/widgets/alert_background.dart index 0b4dab470..1b72597af 100644 --- a/lib/src/widgets/alert_background.dart +++ b/lib/src/widgets/alert_background.dart @@ -1,5 +1,5 @@ import 'dart:ui'; -import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; @@ -21,10 +21,15 @@ class AlertBackground extends StatelessWidget { filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Container( decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), - child: child, + child: Center( + child: Container( + width: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + child: child, + ), + ), ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index 35a5cd45c..e8e20f125 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -13,9 +13,7 @@ class AlertCloseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned( - bottom: 60, - child: GestureDetector( + return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( height: 42, @@ -28,7 +26,6 @@ class AlertCloseButton extends StatelessWidget { child: image ?? closeButton, ), ), - ) ); } } \ No newline at end of file diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 19d43e9c2..ff2102843 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -27,6 +27,7 @@ class BaseTextFormField extends StatelessWidget { this.maxLength, this.focusNode, this.initialValue, + this.onSubmit, this.borderWidth = 1.0}); final TextEditingController? controller; @@ -54,6 +55,7 @@ class BaseTextFormField extends StatelessWidget { final bool? enableInteractiveSelection; final String? initialValue; final double borderWidth; + final void Function(String)? onSubmit; @override Widget build(BuildContext context) { @@ -71,11 +73,12 @@ class BaseTextFormField extends StatelessWidget { inputFormatters: inputFormatters, enabled: enabled, maxLength: maxLength, + onFieldSubmitted: onSubmit, style: textStyle ?? TextStyle( fontSize: 16.0, color: - textColor ?? Theme.of(context).primaryTextTheme!.headline6!.color!), + textColor ?? Theme.of(context).primaryTextTheme.headline6!.color!), decoration: InputDecoration( prefix: prefix, prefixIcon: prefixIcon, @@ -89,17 +92,17 @@ class BaseTextFormField extends StatelessWidget { focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: borderColor ?? - Theme.of(context).primaryTextTheme!.headline6!.backgroundColor!, + Theme.of(context).primaryTextTheme.headline6!.backgroundColor!, width: borderWidth)), disabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: borderColor ?? - Theme.of(context).primaryTextTheme!.headline6!.backgroundColor!, + Theme.of(context).primaryTextTheme.headline6!.backgroundColor!, width: borderWidth)), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: borderColor ?? - Theme.of(context).primaryTextTheme!.headline6!.backgroundColor!, + Theme.of(context).primaryTextTheme.headline6!.backgroundColor!, width: borderWidth))), validator: validator, ); diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index 80461e26d..a59dda905 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.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'; @@ -32,63 +33,62 @@ class CheckBoxPickerState extends State<CheckBoxPicker> { @override Widget build(BuildContext context) { return AlertBackground( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - if (widget.title.isNotEmpty) - 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, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + if (widget.title.isNotEmpty) + 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.headline6!.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: <Widget>[ - (items.length) > 3 - ? Scrollbar( - controller: controller, - child: itemsList(), - ) - : itemsList(), - ], + 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.headline6!.color!, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.65, + maxWidth: ResponsiveLayoutUtil.kPopupWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + items.length > 3 + ? Scrollbar( + controller: controller, + child: itemsList(), + ) + : itemsList(), + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ) - ], - ), - AlertCloseButton(), - ], + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), + AlertCloseButton(), + ], + ), ), ); } diff --git a/lib/src/widgets/market_place_item.dart b/lib/src/widgets/market_place_item.dart index 8049a6346..438391c97 100644 --- a/lib/src/widgets/market_place_item.dart +++ b/lib/src/widgets/market_place_item.dart @@ -17,6 +17,9 @@ class MarketPlaceItem extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: onTap, + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, child: Stack( children: [ Container( diff --git a/lib/src/widgets/nav_bar.dart b/lib/src/widgets/nav_bar.dart index f6d933c8b..aabe8d9c8 100644 --- a/lib/src/widgets/nav_bar.dart +++ b/lib/src/widgets/nav_bar.dart @@ -1,13 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; class NavBar extends StatelessWidget implements ObstructingPreferredSizeWidget { - factory NavBar( - {Widget? leading, - Widget? middle, - Widget? trailing, - Color? backgroundColor}) { - + factory NavBar({Widget? leading, Widget? middle, Widget? trailing, Color? backgroundColor}) { return NavBar._internal( leading: leading, middle: middle, @@ -17,11 +11,7 @@ class NavBar extends StatelessWidget implements ObstructingPreferredSizeWidget { } factory NavBar.withShadow( - {Widget? leading, - Widget? middle, - Widget? trailing, - Color? backgroundColor}) { - + {Widget? leading, Widget? middle, Widget? trailing, Color? backgroundColor}) { return NavBar._internal( leading: leading, middle: middle, @@ -29,13 +19,15 @@ class NavBar extends StatelessWidget implements ObstructingPreferredSizeWidget { height: 80, backgroundColor: backgroundColor, decoration: BoxDecoration( - color: backgroundColor, - boxShadow: [ - BoxShadow( - color: Color.fromRGBO(132, 141, 198, 0.11), - blurRadius: 8, - offset: Offset(0, 2)) - ]), + color: backgroundColor, + boxShadow: [ + BoxShadow( + color: Color.fromRGBO(132, 141, 198, 0.11), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), ); } @@ -59,14 +51,17 @@ class NavBar extends StatelessWidget implements ObstructingPreferredSizeWidget { @override Widget build(BuildContext context) { + if (leading == null && middle == null && trailing == null) { + return const SizedBox(); + } + final pad = height - _originalHeight; final paddingTop = pad / 2; final _paddingBottom = (pad / 2); return Container( decoration: decoration ?? BoxDecoration(color: backgroundColor), - padding: - EdgeInsetsDirectional.only(bottom: _paddingBottom, top: paddingTop), + padding: EdgeInsetsDirectional.only(bottom: _paddingBottom, top: paddingTop), child: CupertinoNavigationBar( leading: leading, automaticallyImplyLeading: false, diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index f26ff3ee2..ccf922d41 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -1,5 +1,6 @@ // ignore_for_file: deprecated_member_use +import 'package:cake_wallet/utils/responsive_layout_util.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'; @@ -70,108 +71,106 @@ class _PickerState<Item> extends State<Picker<Item>> { @override Widget build(BuildContext context) { return AlertBackground( - child: Stack( - alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ - Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - 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, - ), - ), + 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.headline6!.color!, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.65, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.hintText != null) - Padding( - padding: const EdgeInsets.all(16), - child: TextFormField( - controller: searchController, - style: TextStyle(color: Theme.of(context).primaryTextTheme.headline6!.color!), - decoration: InputDecoration( - hintText: widget.hintText, - prefixIcon: Image.asset("assets/images/search_icon.png"), - filled: true, - fillColor: Theme.of(context).accentTextTheme.headline3!.color!, - alignLabelWithHint: false, - contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - ), + ), + ), + 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.headline6!.color!, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.65, + maxWidth: ResponsiveLayoutUtil.kPopupWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.hintText != null) + Padding( + padding: const EdgeInsets.all(16), + child: TextFormField( + controller: searchController, + style: TextStyle(color: Theme.of(context).primaryTextTheme.headline6!.color!), + decoration: InputDecoration( + hintText: widget.hintText, + prefixIcon: Image.asset("assets/images/search_icon.png"), + filled: true, + fillColor: Theme.of(context).accentTextTheme.headline3!.color!, + alignLabelWithHint: false, + contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.transparent, + )), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.transparent, + )), ), ), - Divider( - color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, - height: 1, ), - if (widget.selectedAtIndex != -1) buildSelectedItem(), - Flexible( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - items.length > 3 ? Scrollbar( - controller: controller, - child: itemsList(), - ) : itemsList(), - (widget.description?.isNotEmpty ?? false) - ? Positioned( - bottom: 24, - left: 24, - right: 24, - child: Text( - widget.description!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - fontFamily: 'Lato', - decoration: TextDecoration.none, - color: Theme.of(context).primaryTextTheme.headline6!.color!, - ), + Divider( + color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, + height: 1, + ), + if (widget.selectedAtIndex != -1) buildSelectedItem(), + Flexible( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + items.length > 3 ? Scrollbar( + controller: controller, + child: itemsList(), + ) : itemsList(), + (widget.description?.isNotEmpty ?? false) + ? Positioned( + bottom: 24, + left: 24, + right: 24, + child: Text( + widget.description!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + decoration: TextDecoration.none, + color: Theme.of(context).primaryTextTheme.headline6!.color!, ), - ) - : Offstage(), - ], - ), + ), + ) + : Offstage(), + ], ), - ], - ), - ), + ), + ], ), ), - ) - ], + ), + ), ), + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), AlertCloseButton(), ], ), diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index a563319c5..c27169894 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -24,28 +25,31 @@ class PrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { - final content = SizedBox( - width: double.infinity, - height: 52.0, - child: TextButton( - onPressed: isDisabled - ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: MaterialStateProperty.all<RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), + final content = ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: SizedBox( + width: double.infinity, + height: 52.0, + child: TextButton( + onPressed: isDisabled + ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: MaterialStateProperty.all<RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), + ), ), - ), - overlayColor: MaterialStateProperty.all(Colors.transparent)), - child: Text(text, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor)), - )); + overlayColor: MaterialStateProperty.all(Colors.transparent)), + child: Text(text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled + ? textColor.withOpacity(0.5) + : textColor)), + )), + ); return isDottedBorder ? DottedBorder( @@ -77,29 +81,32 @@ class LoadingPrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 52.0, - child: TextButton( - onPressed: (isLoading || isDisabled) ? null : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: MaterialStateProperty.all<RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - - child: isLoading - ? CupertinoActivityIndicator(animating: true) - : Text(text, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: SizedBox( + width: double.infinity, + height: 52.0, + child: TextButton( + onPressed: (isLoading || isDisabled) ? null : onPressed, + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: MaterialStateProperty.all<RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), + ), )), - )); + + child: isLoading + ? CupertinoActivityIndicator(animating: true) + : Text(text, + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled + ? textColor.withOpacity(0.5) + : textColor + )), + )), + ); } } @@ -130,45 +137,48 @@ class PrimaryIconButton extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 52.0, - child: TextButton( - onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), - shape: MaterialStateProperty.all<RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius), - ), - )), - child: Stack( - children: <Widget>[ - Row( - mainAxisAlignment: mainAxisAlignment, - children: <Widget>[ - Container( - width: 26.0, - height: 52.0, - decoration: BoxDecoration( - shape: BoxShape.circle, color: iconBackgroundColor), - child: Center( - child: Icon(iconData, color: iconColor, size: 22.0) - ), + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: SizedBox( + width: double.infinity, + height: 52.0, + child: TextButton( + onPressed: onPressed, + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), + shape: MaterialStateProperty.all<RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), ), - ], - ), - Container( - height: 52.0, - child: Center( - child: Text(text, - style: TextStyle( - fontSize: 16.0, - color: textColor)), + )), + child: Stack( + children: <Widget>[ + Row( + mainAxisAlignment: mainAxisAlignment, + children: <Widget>[ + Container( + width: 26.0, + height: 52.0, + decoration: BoxDecoration( + shape: BoxShape.circle, color: iconBackgroundColor), + child: Center( + child: Icon(iconData, color: iconColor, size: 22.0) + ), + ), + ], ), - ) - ], - ), - )); + Container( + height: 52.0, + child: Center( + child: Text(text, + style: TextStyle( + fontSize: 16.0, + color: textColor)), + ), + ) + ], + ), + )), + ); } } @@ -190,34 +200,37 @@ class PrimaryImageButton extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 52.0, - child: TextButton( - onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), - shape: MaterialStateProperty.all<RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - child:Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - image, - SizedBox(width: 15), - Text( - text, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: textColor + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + child: SizedBox( + width: double.infinity, + height: 52.0, + child: TextButton( + onPressed: onPressed, + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), + shape: MaterialStateProperty.all<RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), ), - ) - ], - ), - ) - )); + )), + child:Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + image, + SizedBox(width: 15), + Text( + text, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: textColor + ), + ) + ], + ), + ) + )), + ); } } diff --git a/lib/src/widgets/setting_action_button.dart b/lib/src/widgets/setting_action_button.dart new file mode 100644 index 000000000..ef2d4e1bd --- /dev/null +++ b/lib/src/widgets/setting_action_button.dart @@ -0,0 +1,81 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/material.dart'; + +class SettingActionButton extends StatelessWidget { + final bool isLastTile; + final bool isSelected; + final bool isArrowVisible; + final bool selectionActive; + final VoidCallback onTap; + final String image; + final String title; + final double fromBottomEdge; + final double fromTopEdge; + final double tileHeight; + const SettingActionButton({ + super.key, + this.isLastTile = false, + this.isSelected = false, + this.selectionActive = true, + this.isArrowVisible = false, + required this.onTap, + required this.image, + required this.title, + this.tileHeight = 60, + this.fromTopEdge = 50, + this.fromBottomEdge = 25, + }); + + @override + Widget build(BuildContext context) { + Color? color = isSelected + ? Theme.of(context).textTheme.headline3!.color + : selectionActive + ? Palette.darkBlue + : Theme.of(context).textTheme.headline3!.color; + return InkWell( + onTap: onTap, + hoverColor: Colors.transparent, + child: Container( + height: tileHeight, + padding: isLastTile + ? EdgeInsets.only( + left: 24, + right: 24, + top: fromBottomEdge, + ) + : EdgeInsets.only(left: 24, right: 24), + alignment: isLastTile ? Alignment.topLeft : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + Image.asset( + image, + height: 16, + width: 16, + color: Palette.darkBlue, + ), + SizedBox(width: 16), + Expanded( + child: Text( + title, + style: TextStyle( + color: color, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + if (isArrowVisible) + Icon( + Icons.arrow_forward_ios, + color: color, + size: 16, + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart new file mode 100644 index 000000000..4dd16670a --- /dev/null +++ b/lib/src/widgets/setting_actions.dart @@ -0,0 +1,109 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:flutter/material.dart'; + +class SettingActions { + final String name; + final String image; + final void Function(BuildContext) onTap; + + SettingActions._({ + required this.name, + required this.image, + required this.onTap, + }); + + static List<SettingActions> all = [ + connectionSettingAction, + walletSettingAction, + addressBookSettingAction, + securityBackupSettingAction, + privacySettingAction, + displaySettingAction, + otherSettingAction, + supportSettingAction, + ]; + + static List<SettingActions> desktopSettings = [ + connectionSettingAction, + walletSettingAction, + addressBookSettingAction, + securityBackupSettingAction, + privacySettingAction, + displaySettingAction, + otherSettingAction, + supportSettingAction, + ]; + + static SettingActions connectionSettingAction = SettingActions._( + name: S.current.connection_sync, + image: 'assets/images/nodes_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.connectionSync); + }, + ); + + static SettingActions walletSettingAction = SettingActions._( + name: S.current.wallets, + image: 'assets/images/wallet_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.walletList); + }, + ); + + static SettingActions addressBookSettingAction = SettingActions._( + name: S.current.address_book_menu, + image: 'assets/images/open_book_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.addressBook); + }, + ); + + static SettingActions securityBackupSettingAction = SettingActions._( + name: S.current.security_and_backup, + image: 'assets/images/key_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.securityBackupPage); + }, + ); + + static SettingActions privacySettingAction = SettingActions._( + name: S.current.privacy, + image: 'assets/images/privacy_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.privacyPage); + }, + ); + + static SettingActions displaySettingAction = SettingActions._( + name: S.current.display_settings, + image: 'assets/images/eye_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.displaySettingsPage); + }, + ); + + static SettingActions otherSettingAction = SettingActions._( + name: S.current.other_settings, + image: 'assets/images/settings_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.otherSettingsPage); + }, + ); + + static SettingActions supportSettingAction = SettingActions._( + name: S.current.settings_support, + image: 'assets/images/question_mark.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.support); + }, + ); +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index b6e5a7549..02eb51da7 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -241,8 +241,8 @@ abstract class SettingsStoreBase with Store { {required Box<Node> nodeSource, required bool isBitcoinBuyEnabled, FiatCurrency initialFiatCurrency = FiatCurrency.usd, - BalanceDisplayMode initialBalanceDisplayMode = - BalanceDisplayMode.availableBalance}) async { + BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance, + ThemeBase? initialTheme}) async { final sharedPreferences = await getIt.getAsync<SharedPreferences>(); final currentFiatCurrency = FiatCurrency.deserialize(raw: @@ -292,7 +292,7 @@ abstract class SettingsStoreBase with Store { (sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy) ?? false) ? ThemeType.dark.index : ThemeType.bright.index; - final savedTheme = ThemeList.deserialize( + final savedTheme = initialTheme ?? ThemeList.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? legacyTheme); final actionListDisplayMode = ObservableList<ActionListDisplayMode>(); diff --git a/lib/utils/device_info.dart b/lib/utils/device_info.dart new file mode 100644 index 000000000..144ea3fa4 --- /dev/null +++ b/lib/utils/device_info.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +class DeviceInfo { + DeviceInfo._(); + + static DeviceInfo get instance => DeviceInfo._(); + + bool get isMobile => Platform.isAndroid || Platform.isIOS; + + bool get isDesktop => Platform.isMacOS || Platform.isWindows || Platform.isLinux; +} \ No newline at end of file diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 002534ea1..d3689e7e0 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -69,6 +69,7 @@ class ExceptionHandler { static void onError(FlutterErrorDetails errorDetails) async { if (kDebugMode) { FlutterError.presentError(errorDetails); + debugPrint(errorDetails.toString()); return; } diff --git a/lib/utils/responsive_layout_util.dart b/lib/utils/responsive_layout_util.dart new file mode 100644 index 000000000..8ae76ca21 --- /dev/null +++ b/lib/utils/responsive_layout_util.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class ResponsiveLayoutUtil { + static const double _kMobileThreshold = 900; + static const double kDesktopMaxWidthConstraint = 400; + static const double kPopupWidth = 400; + static const double kPopupSpaceHeight = 100; + + + const ResponsiveLayoutUtil._(); + + static final instance = ResponsiveLayoutUtil._(); + + bool isMobile(BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return mediaQueryData.size.width < _kMobileThreshold; + } + + /// Returns dynamic size. + /// + /// If screen size is mobile, it returns 66% ([scale]) of the [originalValue]. + double getDynamicSize( + BuildContext context, + double originalValue, { + double? mobileSize, + double? scale, + }) { + scale ??= 2 / 3; + mobileSize ??= originalValue * scale; + final value = isMobile(context) ? mobileSize : originalValue; + + return value.roundToDouble(); + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 5384ee743..b23f430f9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,13 +1,9 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; -import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; -import 'package:cake_wallet/buy/order.dart'; -import 'package:cake_wallet/entities/transaction_history.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -21,10 +17,6 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.dart'; -import 'package:crypto/crypto.dart'; -import 'package:flutter/services.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; diff --git a/lib/view_model/dashboard/desktop_sidebar_view_model.dart b/lib/view_model/dashboard/desktop_sidebar_view_model.dart new file mode 100644 index 000000000..d0320c05f --- /dev/null +++ b/lib/view_model/dashboard/desktop_sidebar_view_model.dart @@ -0,0 +1,34 @@ +import 'package:mobx/mobx.dart'; + +part 'desktop_sidebar_view_model.g.dart'; + +enum SidebarItem { + dashboard, + support, + settings, + transactions; +} + +class DesktopSidebarViewModel = DesktopSidebarViewModelBase with _$DesktopSidebarViewModel; + +abstract class DesktopSidebarViewModelBase with Store { + DesktopSidebarViewModelBase(); + + @observable + SidebarItem currentPage = SidebarItem.dashboard; + + @action + void onPageChange(SidebarItem item) { + if (currentPage == item) { + resetSidebar(); + + return; + } + currentPage = item; + } + + @action + void resetSidebar() { + currentPage = SidebarItem.dashboard; + } +} diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index deb1f29cf..3663d48ac 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/utils/mobx.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; @@ -7,25 +9,28 @@ import 'package:cw_core/node.dart'; import 'package:cake_wallet/entities/node_list.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/utils/mobx.dart'; part 'node_list_view_model.g.dart'; class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel; abstract class NodeListViewModelBase with Store { - NodeListViewModelBase(this._nodeSource, this.wallet, this.settingsStore) - : nodes = ObservableList<Node>() { - _nodeSource.bindToList(nodes, - filter: (Node val) => val?.type == wallet.type, initialFire: true); + NodeListViewModelBase(this._nodeSource, this._appStore) + : nodes = ObservableList<Node>(), + settingsStore = _appStore.settingsStore { + _bindNodes(); + + reaction((_) => _appStore.wallet, (WalletBase? _wallet) { + _bindNodes(); + }); } @computed Node get currentNode { - final node = settingsStore.nodes[wallet.type]; + final node = settingsStore.nodes[_appStore.wallet!.type]; if (node == null) { - throw Exception('No node for wallet type: ${wallet.type}'); + throw Exception('No node for wallet type: ${_appStore.wallet!.type}'); } return node; @@ -33,19 +38,19 @@ abstract class NodeListViewModelBase with Store { String getAlertContent(String uri) => S.current.change_current_node(uri) + - '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + S.current.orbot_running_alert : ''}'; + '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + S.current.orbot_running_alert : ''}'; final ObservableList<Node> nodes; final SettingsStore settingsStore; - final WalletBase wallet; final Box<Node> _nodeSource; + final AppStore _appStore; Future<void> reset() async { await resetToDefault(_nodeSource); Node node; - switch (wallet.type) { + switch (_appStore.wallet!.type) { case WalletType.bitcoin: node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; break; @@ -59,7 +64,7 @@ abstract class NodeListViewModelBase with Store { node = getHavenDefaultNode(nodes: _nodeSource)!; break; default: - throw Exception('Unexpected wallet type: ${wallet.type}'); + throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } await setAsCurrent(node); @@ -68,6 +73,15 @@ abstract class NodeListViewModelBase with Store { @action Future<void> delete(Node node) async => node.delete(); - Future<void> setAsCurrent(Node node) async => - settingsStore.nodes[wallet.type] = node; + Future<void> setAsCurrent(Node node) async => settingsStore.nodes[_appStore.wallet!.type] = node; + + @action + void _bindNodes() { + nodes.clear(); + _nodeSource.bindToList( + nodes, + filter: (val) => val.type == _appStore.wallet!.type, + initialFire: true, + ); + } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 023713bd8..e20089915 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -12,70 +13,83 @@ part 'wallet_keys_view_model.g.dart'; class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel; abstract class WalletKeysViewModelBase with Store { - WalletKeysViewModelBase(WalletBase wallet) - : title = wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin + WalletKeysViewModelBase(this._appStore) + : title = _appStore.wallet!.type == WalletType.bitcoin || + _appStore.wallet!.type == WalletType.litecoin ? S.current.wallet_seed : S.current.wallet_keys, - _wallet = wallet, - _restoreHeight = wallet.walletInfo.restoreHeight, + _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, items = ObservableList<StandartListItem>() { - if (wallet.type == WalletType.monero) { - final keys = monero!.getKeys(wallet); - items.addAll([ - if (keys['publicSpendKey'] != null) - StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), - if (keys['privateSpendKey'] != null) - StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), - if (keys['publicViewKey'] != null) - StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), - if (keys['privateViewKey'] != null) - StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem(title: S.current.wallet_seed, value: wallet.seed), - ]); - } + _populateItems(); - if (wallet.type == WalletType.haven) { - final keys = haven!.getKeys(wallet); - items.addAll([ - if (keys['publicSpendKey'] != null) - StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), - if (keys['privateSpendKey'] != null) - StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), - if (keys['publicViewKey'] != null) - StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), - if (keys['privateViewKey'] != null) - StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem(title: S.current.wallet_seed, value: wallet.seed), - ]); - } - - if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { - items.addAll([ - StandartListItem(title: S.current.wallet_seed, value: wallet.seed), - ]); - } + reaction((_) => _appStore.wallet, (WalletBase? _wallet) { + _populateItems(); + }); } final ObservableList<StandartListItem> items; final String title; - final WalletBase _wallet; + final AppStore _appStore; final int _restoreHeight; + void _populateItems() { + items.clear(); + + if (_appStore.wallet!.type == WalletType.monero) { + final keys = monero!.getKeys(_appStore.wallet!); + + items.addAll([ + if (keys['publicSpendKey'] != null) + StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), + if (keys['privateSpendKey'] != null) + StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), + if (keys['publicViewKey'] != null) + StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), + if (keys['privateViewKey'] != null) + StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed), + ]); + } + + if (_appStore.wallet!.type == WalletType.haven) { + final keys = haven!.getKeys(_appStore.wallet!); + + items.addAll([ + if (keys['publicSpendKey'] != null) + StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), + if (keys['privateSpendKey'] != null) + StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), + if (keys['publicViewKey'] != null) + StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), + if (keys['privateViewKey'] != null) + StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed), + ]); + } + + if (_appStore.wallet!.type == WalletType.bitcoin || + _appStore.wallet!.type == WalletType.litecoin) { + items.addAll([ + StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed), + ]); + } + } + Future<int?> currentHeight() async { - if (_wallet.type == WalletType.haven) { + if (_appStore.wallet!.type == WalletType.haven) { return await haven!.getCurrentHeight(); } - if (_wallet.type == WalletType.monero) { + if (_appStore.wallet!.type == WalletType.monero) { return monero_wallet.getCurrentHeight(); } return null; } String get _path { - switch (_wallet.type) { + switch (_appStore.wallet!.type) { case WalletType.monero: return 'monero_wallet:'; case WalletType.bitcoin: @@ -85,7 +99,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.haven: return 'haven_wallet:'; default: - throw Exception('Unexpected wallet type: ${_wallet.toString()}'); + throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } } @@ -103,7 +117,7 @@ abstract class WalletKeysViewModelBase with Store { Future<Map<String, String>> get _queryParams async { final restoreHeightResult = await restoreHeight; return { - 'seed': _wallet.seed, + 'seed': _appStore.wallet!.seed, if (restoreHeightResult != null) ...{'height': restoreHeightResult} }; } diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index af30b9bea..a644c07b3 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -1,13 +1,13 @@ -import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_type.dart'; class WalletListItem { - const WalletListItem( - {required this.name, - required this.type, - required this.key, - this.isCurrent = false, - this.isEnabled = true}); + const WalletListItem({ + required this.name, + required this.type, + required this.key, + this.isCurrent = false, + this.isEnabled = true, + }); final String name; final WalletType type; diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 6d63675ba..be8d928aa 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -22,6 +22,7 @@ abstract class WalletListViewModelBase with Store { this._authService, ) : wallets = ObservableList<WalletListItem>() { _updateList(); + reaction((_) => _appStore.wallet, (_) => _updateList()); } @observable diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/CakeWallet/secRandom.swift b/macos/CakeWallet/secRandom.swift new file mode 100644 index 000000000..c9b2e3593 --- /dev/null +++ b/macos/CakeWallet/secRandom.swift @@ -0,0 +1,12 @@ +import Foundation + +func secRandom(count: Int) -> Data? { + var bytes = [Int8](repeating: 0, count: count) + let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + + if status == errSecSuccess { + return Data(bytes: bytes, count: bytes.count) + } + + return nil +} diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..feebda3f2 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,36 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import connectivity_macos +import cw_monero +import device_info_plus +import devicelocale +import flutter_secure_storage_macos +import package_info +import path_provider_foundation +import platform_device_id +import platform_device_id_macos +import share_plus_macos +import shared_preferences_foundation +import url_launcher_macos +import wakelock_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) + FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) + FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) + PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 000000000..0c76ccf54 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 000000000..41861493e --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,118 @@ +PODS: + - connectivity_macos (0.0.1): + - FlutterMacOS + - Reachability + - cw_monero (0.0.1): + - cw_monero/Boost (= 0.0.1) + - cw_monero/Monero (= 0.0.1) + - cw_monero/OpenSSL (= 0.0.1) + - cw_monero/Sodium (= 0.0.1) + - cw_monero/Unbound (= 0.0.1) + - FlutterMacOS + - cw_monero/Boost (0.0.1): + - FlutterMacOS + - cw_monero/Monero (0.0.1): + - FlutterMacOS + - cw_monero/OpenSSL (0.0.1): + - FlutterMacOS + - cw_monero/Sodium (0.0.1): + - FlutterMacOS + - cw_monero/Unbound (0.0.1): + - FlutterMacOS + - device_info_plus (0.0.1): + - FlutterMacOS + - devicelocale (0.0.1): + - FlutterMacOS + - flutter_secure_storage_macos (3.3.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - package_info (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - platform_device_id (0.0.1): + - FlutterMacOS + - platform_device_id_macos (0.0.1): + - FlutterMacOS + - Reachability (3.2) + - share_plus_macos (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - connectivity_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos`) + - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) + - platform_device_id (from `Flutter/ephemeral/.symlinks/plugins/platform_device_id/macos`) + - platform_device_id_macos (from `Flutter/ephemeral/.symlinks/plugins/platform_device_id_macos/macos`) + - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) + +SPEC REPOS: + trunk: + - Reachability + +EXTERNAL SOURCES: + connectivity_macos: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos + cw_monero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + devicelocale: + :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info: + :path: Flutter/ephemeral/.symlinks/plugins/package_info/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos + platform_device_id: + :path: Flutter/ephemeral/.symlinks/plugins/platform_device_id/macos + platform_device_id_macos: + :path: Flutter/ephemeral/.symlinks/plugins/platform_device_id_macos/macos + share_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_macos: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos + +SPEC CHECKSUMS: + connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9 + cw_monero: f8b7f104508efba2591548e76b5c058d05cba3f0 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 + flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + platform_device_id: 3e414428f45df149bbbfb623e2c0ca27c545b763 + platform_device_id_macos: f763bb55f088be804d61b96eb4710b8ab6598e94 + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 + share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 + shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 + url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 + +PODFILE CHECKSUM: 505596d150d38022472859d890f709281982e016 + +COCOAPODS: 1.11.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..06558bf57 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,663 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4171CB1F5A4EA2E4DC33F52F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */; }; + 9F565D5929954F53009A75FB /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F565D5729954F53009A75FB /* secRandom.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 094BF982245FD1012D60A103 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; + 0C090639294D3AAC00954DC9 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; + 2A820A13B0719E9E0CD6686F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; + 33CC10ED2044A3C60003C045 /* Cake Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Cake Wallet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + 9646C67C7114830A5ACFF5DF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; + 9F565D5729954F53009A75FB /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = secRandom.swift; path = CakeWallet/secRandom.swift; sourceTree = "<group>"; }; + 9F565D5829954F53009A75FB /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = decrypt.swift; path = CakeWallet/decrypt.swift; sourceTree = "<group>"; }; + B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4171CB1F5A4EA2E4DC33F52F /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = "<group>"; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 9F565D5829954F53009A75FB /* decrypt.swift */, + 9F565D5729954F53009A75FB /* secRandom.swift */, + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 9B6E7CA3983216A9E173F00F /* Pods */, + ); + sourceTree = "<group>"; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* Cake Wallet.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = "<group>"; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = "<group>"; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = "<group>"; + }; + 9B6E7CA3983216A9E173F00F /* Pods */ = { + isa = PBXGroup; + children = ( + 9646C67C7114830A5ACFF5DF /* Pods-Runner.debug.xcconfig */, + 2A820A13B0719E9E0CD6686F /* Pods-Runner.release.xcconfig */, + 094BF982245FD1012D60A103 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = "<group>"; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0C090639294D3AAC00954DC9 /* libiconv.tbd */, + B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 93B711AB4B96E7C8C5C5B844 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 5592D00118C2EA3C5E0B5FDF /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* Cake Wallet.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 5592D00118C2EA3C5E0B5FDF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 93B711AB4B96E7C8C5C5B844 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F565D5929954F53009A75FB /* secRandom.swift in Sources */, + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = 32J6BB6VUS; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Cake Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0.1; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = 32J6BB6VUS; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Cake Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0.1; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = 32J6BB6VUS; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Cake Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12; + MARKETING_VERSION = 1.0.1; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/macos/Runner.xcodeproj/project_base.pbxproj b/macos/Runner.xcodeproj/project_base.pbxproj new file mode 100644 index 000000000..fb3e76252 --- /dev/null +++ b/macos/Runner.xcodeproj/project_base.pbxproj @@ -0,0 +1,636 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 328F945957E1041662291EC5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C84AA35EA80D710889C68D81 /* Pods_Runner.framework */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C090639294D3AAC00954DC9 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; + 135D3AD0276D31F62BBEDDBF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; + 33CC10ED2044A3C60003C045 /* cake_wallet_new.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cake_wallet_new.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; }; + 359F2F22842E234537DED5E3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; + C84AA35EA80D710889C68D81 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FF499CFF131B036E3C5638D0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 328F945957E1041662291EC5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = "<group>"; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 9B6E7CA3983216A9E173F00F /* Pods */, + ); + sourceTree = "<group>"; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* cake_wallet_new.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = "<group>"; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = "<group>"; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = "<group>"; + }; + 9B6E7CA3983216A9E173F00F /* Pods */ = { + isa = PBXGroup; + children = ( + 359F2F22842E234537DED5E3 /* Pods-Runner.debug.xcconfig */, + 135D3AD0276D31F62BBEDDBF /* Pods-Runner.release.xcconfig */, + FF499CFF131B036E3C5638D0 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = "<group>"; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0C090639294D3AAC00954DC9 /* libiconv.tbd */, + C84AA35EA80D710889C68D81 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 8AB41FC42599228A92F51A44 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + F015812745AAC61FF550BB30 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* cake_wallet_new.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 8AB41FC42599228A92F51A44 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F015812745AAC61FF550BB30 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = ; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = ; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ARCHS = ; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..8536e9a81 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1300" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "Cake Wallet.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "Cake Wallet.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "Cake Wallet.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Profile" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "Cake Wallet.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "group:Runner.xcodeproj"> + </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> +</Workspace> diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreviewsEnabled</key> + <false/> +</dict> +</plist> diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..0c8973175 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,33 @@ +import Cocoa +import FlutterMacOS +import IOKit.pwr_mgt + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationDidFinishLaunching(_ notification: Notification) { + let controller : FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController + + let utilsChannel = FlutterMethodChannel( + name: "com.cake_wallet/native_utils", + binaryMessenger: controller.engine.binaryMessenger) + utilsChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + switch call.method { + case "sec_random": + guard let args = call.arguments as? Dictionary<String, Any>, + let count = args["count"] as? Int else { + result(nil) + return + } + + result(secRandom(count: count)) + + default: + result(FlutterMethodNotImplemented) + } + }) + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..96d3fee1a --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "info": { + "version": 1, + "author": "xcode" + }, + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..73101354a94f8a498125cce07b600e77716908b9 GIT binary patch literal 112502 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%0D;JkNN+MqBocLt_iy%RW+p&eoMc~h|;6P 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..9ceee3c5eb2df653ceaa19d223a25a5f281fa64a GIT binary patch literal 7683 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ef46cd8054c4211cd2b16f739ec84a3e517bf404 GIT binary patch literal 678 zcmV;X0$KfuP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0007QNkl<Zcmaiy zdq`7J9LK-+-YuPvP@`G)pc<_|vZ<>;X!g=p`Nk3|iSm(KOl&lxK#*bui=vUJivklJ zT41;gOolx$um^@MOK_zbTIdQ))72h#x4RXqZ5R5&56=0Vb3Po-@9^-y@c01j1{j8W znUikXy#rb0N+fPcLsYB~4*M+L4fLa@rJ1cZR!DOT!Xv_=X}p27f+BWC{N<6c*dW{G ztK8!iI9Iq2wvV3z1tdkHroA1~j4VH+-40AoO&~gMF~S53eT*L#0oPAw=ne8Kl>WnR zmjgy^H7t+JSh7Zp>B-Mnl&}nYOBJ|q@DMBg5-1=k0{c&%{=*ntRUW}=NfNH~z5+pz zKKi=XjGDZB*uT$*KH^%b1aV7O`o<%c7x*zZSD;j%G1(ZRKuDm&#vmtso4j*Od)CvS zJ{2lR5Q+R`@JTOHvvZT931Wuro?$(OS(--4auTN0>>O|k7>~zKD2js1>9{@V!L1}7 zE`RXgl9<Bv33iLi;Y3h=3&w}7ev-U119cV3w|k0JqjrX@ANN2KP2kQHD|pyy<Q9w$ z4q<A{iuhGxtXL<3C?y4Tm8E%QIz0qYF+~=q8{IbzFxAy_3mDekrqe=wt;IJ#rmMw0 zgC3ggn?doz*}l(XqdXH$!z0iuO3-(=D^TD~ccT%~bUC&b9tJ^D$W>LbrGeXNXC(rE zJFD9|aQ;9(o_5|36nJy-XfeKhwIN5L_DQ_onYSSFpB)S=kIfkBdlq^FZ)g{dh7(mV zH#H%%tQ?6l8DfO-u+PrG`m!GnjLmr7)f4&#!FfOM2DPe6&b6TY0wpxtzd#?_&;S4c M07*qoM6N<$g1_N1asU7T literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..6547a1b1b312dcf34c5eb6150f17c5a8e0c42919 GIT binary patch literal 17249 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..e436872e8d5e056b845a19c07e49ada864d0dc01 GIT binary patch literal 1522 zcmV<O1r7R%P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F8000HKNkl<Zcmc(i zdr(wW9LK-+?z--R0-Lf5u_~+&kPK$WfeggVfP9n`jtGiMJZuslI4Xi*7)}blSF#yG zoiSeGBf%G&U`_~@5~(~=aur=N1XN^^g=Ked2a*!@3cKpmZ|3}Q@BN+g{hV`u=XZXG zi~rfUFV~#{czl4v0Xo_NZO4BA;55k@9)2Ie`K`$iJ2}D3LIj%TLetia#@gHXy{r_a zXY+C4*lDO$?XM2N!*3equTOx3iwC-<-phD=A#6u?HYujejlgF+8gVLZ4|0-sp!q?| zivtLUSs^NQFWjcjV-<d$2?h_voT!cPUJwS^(hw9MFEkxsl(Phhd8c759&5C4Z?m)( zBQ9Hx^i8Xfu}Nk;fY@;)Hs_xwD`5A+;@zfa^tfMhkL?pXfSCm!2}g7L5ogfS^atv0 zRYTLR!D}|dArg-V&EfU%(&;*|Gdu_d=|_wQ2v17H>tkHlW%XJV9Xo*Idt|tNsRDZ4 zzu$%ox4_h(0Qg7668KU5iL(QYc9p<)@d{S&<_Gt%CD<Qj1wRo$8w9RfiP2Ii_~rsM z+*RVTT#n-@X~^H7iB+j7D9Jn3mpD6sf7EKSi~|hbw>CFmt<MxFe^ZbZiih8_Sa7)f z9zH&cqoBM|hOHsN_+oBIU&8DF=9UA=eh*~vW3YE+IBF_xVtvLpm^>%!xg(0lNs;vZ z1;qGz;ritY(*RuF@dQs`Y2c-}dI5zQxtJLqNW???8x(>;LafWk#u67-w5zqo130?5 zv3eDJd!Q=^1+0ya8bMQ^ifOoB)IzNX$TL=qq%omH3l)jSrFJxyk!R_}QO>PX13xD5 zkYhZ6twdtz!Z0W=DuTqt0iqF(=N{^@-H`+DAr$f&7zEo=7->df)=&yr_ZiHyp>X{P zgH{3u_nEUy0~jQ<GISv`Z&21^f>+lWaFt~BY9jEVHI0fE9b7FaDAf%7htnu{sKX>7 zg@xCiZcSTz`@X~t0T?~Q>P3_DwnL-oemqpCM+Er|idGtT)j*;`2bmKG0mEptlbN4r zcsk58eT)Nm+}OzCL%~MkfSU43@;7v#D9&L55HF#z+MdR~T0M@|Ge{W^B#fbPhKQ@W zbJMNzYo-AxZ&b5-NvBOidg3lrmB``jF{7e|K^6HG;|Ed5CNNDT&gG2^W|I}jsrx50 zGB}@|Wg0+LNinOJ&!PxqC&`c&6N7Dq?}9UczxkF99##~}RGrIBVb2{sR0QN?_wrvl zlZ~>%OQr!-mXt#E=pF_OZF{^Ijc~%;)se{jQU=+wg;@SodPW_Gmr?h(c&8OUVn};B zd&QL^k_{vJ6E_6VwCPF0*$2PKcmvP*amlEutbly~Vf?AMjV0T5kge&7XN#0}brVjd z?!v)@%_L28VQnWuLv6kB0G)PhmkiTGmt$bBjL6{*L~Q0s9Ee+v?{=qQshbxZ-n1u8 z3IREc>Xk}V7XM6+=r-6-kRbYCCQ0o=#7_4nv319Ifcm?Q$lel<MH>yO7jOp%$U&5f z_ZNpD_v;jt=I7&wtfO5=vbhBpV<%38_reA6n)3;X6G2y>t%XI1`@kDFf4yNGK&Kr_ zP9c%+Lz2%dmai14hTRb<b4O>SYt#>+)wY9gHq@XP=`<^mm>f<&V#91-5@T141JLUj zBnAYLj%Ueoy0iLp!|(({U?|(u%q>OOm?uZ5qlDy|9u@NQYr^ISDn$GG;PWFnaDLmj zABn!L?MV}MM+Em4?;D^CqOlp#J~I%!VGaCOZUC2OW}-BM>eBPr9u|r_Rkzt3`o9-w zHF^^HNjOWQIw&R@p8mn)5@OM>a7}qJj_llqb6L4P>PC|Rx~;ZaiR6$->|M2nTvFb{ z8y=pp8|MTo>tSSNxOmd^nCze$RR2P*W~4S$TvZq|tm${dmPcxw%RJH5UV**j?fB*G YFD>>OFA+L-4gdfE07*qoM6N<$f{n1t7XSbN literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..157b004934f008ce3a79c67eb80432818eb5255e GIT binary patch literal 42348 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 VlSCQZ>7TeDEj2yWdS%;~{{feJuIB&% literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..a46ed4535c63ac6c4de889181f173bb4cd58a43b GIT binary patch literal 3520 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..a6981c270 --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,344 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Cake_Wallet" customModuleProvider="target"> + <connections> + <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/> + <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> + </connections> + </customObject> + <customObject id="YLy-65-1bz" customClass="NSFontManager"/> + <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="APP_NAME" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About APP_NAME" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="-1" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="-1" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="-1" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="-1" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="-1" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="-1" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="-1" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="EPT-qC-fAb"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/> + </menuItem> + </items> + <point key="canvasLocation" x="142" y="-258"/> + </menu> + <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Cake_Wallet" customModuleProvider="target"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> + <rect key="contentRect" x="335" y="390" width="1163" height="755"/> + <rect key="screenRect" x="0.0" y="0.0" width="1512" height="944"/> + <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> + <rect key="frame" x="0.0" y="0.0" width="1163" height="755"/> + <autoresizingMask key="autoresizingMask"/> + </view> + <point key="canvasLocation" x="138.5" y="222.5"/> + </window> + </objects> +</document> diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..84f96bff7 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = Cake Wallet + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.fotolockr. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfileBase.entitlements b/macos/Runner/DebugProfileBase.entitlements new file mode 100644 index 000000000..c1b4345fe --- /dev/null +++ b/macos/Runner/DebugProfileBase.entitlements @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>com.apple.security.cs.allow-jit</key> + <true/> + <key>com.apple.security.network.server</key> + <true/> + <key>keychain-access-groups</key> + <array> + <string>$(AppIdentifierPrefix)${BUNDLE_ID}</string> + </array> + <key>com.apple.security.network.client</key> + <true/> +</dict> +</plist> diff --git a/macos/Runner/InfoBase.plist b/macos/Runner/InfoBase.plist new file mode 100644 index 000000000..98d0ea9ee --- /dev/null +++ b/macos/Runner/InfoBase.plist @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>$(FLUTTER_BUILD_NAME)</string> + <key>CFBundleVersion</key> + <string>$(FLUTTER_BUILD_NUMBER)</string> + <key>LSMinimumSystemVersion</key> + <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>NSHumanReadableCopyright</key> + <string>$(PRODUCT_COPYRIGHT)</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>LSApplicationCategoryType</key> + <string>public.app-category.finance</string> +</dict> +</plist> diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/ReleaseBase.entitlements b/macos/Runner/ReleaseBase.entitlements new file mode 100644 index 000000000..aef6ac342 --- /dev/null +++ b/macos/Runner/ReleaseBase.entitlements @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>keychain-access-groups</key> + <array> + <string>$(AppIdentifierPrefix)${BUNDLE_ID}</string> + </array> + <key>com.apple.security.network.client</key> + <true/> +</dict> +</plist> diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 7825824a9..b357619bd 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -9,6 +9,7 @@ dependencies: qr_flutter: ^4.0.0 uuid: 3.0.6 shared_preferences: ^2.0.15 + shared_preferences_android: 2.1.0 flutter_secure_storage: git: url: https://github.com/cake-tech/flutter_secure_storage.git @@ -34,7 +35,9 @@ dependencies: local_auth: ^2.1.0 package_info: ^2.0.0 #package_info_plus: ^1.4.2 - devicelocale: ^0.4.3 + devicelocale: + git: + url: https://github.com/OmarHatem28/flutter-devicelocale auto_size_text: ^3.0.0 dotted_border: ^2.0.0+2 smooth_page_indicator: ^1.0.0+2 @@ -61,6 +64,7 @@ dependencies: permission_handler: ^10.0.0 device_display_brightness: ^0.0.6 platform_device_id: ^1.0.1 + wakelock: ^0.6.2 flutter_mailer: ^2.0.2 device_info_plus: 8.1.0 cake_backup: @@ -72,11 +76,11 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.3.3 mobx_codegen: ^2.1.1 build_resolvers: ^2.0.9 hive_generator: ^1.1.3 - flutter_launcher_icons: ^0.9.3 + flutter_launcher_icons: ^0.11.0 # check flutter_launcher_icons for usage pedantic: ^1.8.0 # replace https://github.com/dart-lang/lints#migrating-from-packagepedantic @@ -85,6 +89,9 @@ flutter_icons: image_path: "assets/images/app_logo.png" android: true ios: true + macos: + generate: true + image_path: "assets/images/app_logo.png" flutter: uses-material-design: true diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index e06846872..430f2edcf 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -695,5 +695,6 @@ "optional_name": "اسم المستلم الاختياري", "clearnet_link": "رابط Clearnet", "onion_link": "رابط البصل", + "settings": "إعدادات", "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 6a8f3c89e..3896fb133 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -697,5 +697,6 @@ "optional_name": "Optionaler Empfängername", "clearnet_link": "Clearnet-Link", "onion_link": "Zwiebel-Link", + "settings": "Einstellungen", "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b1bf94690..fd3cdbc99 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -697,5 +697,6 @@ "onion_link": "Onion link", "decimal_places_error": "Too many decimal places", "edit_node": "Edit Node", + "settings": "Settings", "sell_monero_com_alert_content": "Selling Monero is not supported yet" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 8e9e4992c..2995008ac 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -697,5 +697,6 @@ "optional_name": "Nombre del destinatario opcional", "clearnet_link": "enlace Clearnet", "onion_link": "Enlace de cebolla", + "settings": "Configuraciones", "sell_monero_com_alert_content": "Aún no se admite la venta de Monero" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 9ba3066c1..4dd8e54cb 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -696,6 +696,7 @@ "optional_description": "Descriptif facultatif", "optional_name": "Nom du destinataire facultatif", "clearnet_link": "Lien Clearnet", + "settings": "Paramètres", "onion_link": "Lien .onion", "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index ea9a673a8..e8280bae7 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -697,5 +697,6 @@ "optional_name": "वैकल्पिक प्राप्तकर्ता नाम", "clearnet_link": "क्लियरनेट लिंक", "onion_link": "प्याज का लिंक", + "settings": "समायोजन", "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8a0b4b5ed..973aaca59 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -697,5 +697,6 @@ "optional_name": "Izborno ime primatelja", "clearnet_link": "Clearnet veza", "onion_link": "Poveznica luka", + "settings": "Postavke", "sell_monero_com_alert_content": "Prodaja Monera još nije podržana" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 968fb991d..4b90f8890 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -697,5 +697,6 @@ "optional_name": "Nome del destinatario facoltativo", "clearnet_link": "Collegamento Clearnet", "onion_link": "Collegamento a cipolla", + "settings": "Impostazioni", "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 3c45e803d..7caa1b170 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -697,5 +697,6 @@ "optional_name": "オプションの受信者名", "clearnet_link": "クリアネット リンク", "onion_link": "オニオンリンク", + "settings": "設定", "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 1535f54b2..327da90db 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -697,5 +697,6 @@ "optional_name": "선택적 수신자 이름", "clearnet_link": "클리어넷 링크", "onion_link": "양파 링크", + "settings": "설정", "sell_monero_com_alert_content": "지원되지 않습니다." } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 5020cc966..aaa19d5e7 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -697,5 +697,6 @@ "optional_name": "ရွေးချယ်နိုင်သော လက်ခံသူအမည်", "clearnet_link": "Clearnet လင့်ခ်", "onion_link": "ကြက်သွန်လင့်", + "settings": "ဆက်တင်များ", "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9b19f6e84..875c4e5c0 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -697,5 +697,6 @@ "optional_name": "Optionele naam ontvanger", "clearnet_link": "Clearnet-link", "onion_link": "Ui koppeling", + "settings": "Instellingen", "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index cdc1c916a..b2b1fcfa5 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -697,5 +697,6 @@ "optional_name": "Opcjonalna nazwa odbiorcy", "clearnet_link": "łącze Clearnet", "onion_link": "Łącznik cebulowy", + "settings": "Ustawienia", "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 86a256dcc..3ac194e7e 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -696,5 +696,6 @@ "optional_name": "Nome do destinatário opcional", "clearnet_link": "link clear net", "onion_link": "ligação de cebola", + "settings": "Configurações", "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 7707e0003..d9d5e1d4f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -697,5 +697,6 @@ "optional_name": "Необязательное имя получателя", "clearnet_link": "Клирнет ссылка", "onion_link": "Луковая ссылка", + "settings": "Настройки", "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 01a05204e..4b400caea 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -695,5 +695,6 @@ "optional_name": "ชื่อผู้รับเพิ่มเติม", "clearnet_link": "ลิงค์เคลียร์เน็ต", "onion_link": "ลิงค์หัวหอม", + "settings": "การตั้งค่า", "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 2df815175..b8fd76499 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -697,5 +697,6 @@ "optional_name": "İsteğe bağlı alıcı adı", "clearnet_link": "Net bağlantı", "onion_link": "soğan bağlantısı", + "settings": "ayarlar", "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ee7c4e507..1b9995893 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -696,5 +696,6 @@ "optional_name": "Додаткове ім'я одержувача", "clearnet_link": "Посилання Clearnet", "onion_link": "Посилання на цибулю", + "settings": "Налаштування", "sell_monero_com_alert_content": "Продаж Monero ще не підтримується" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index fcaf66b6a..54c3c14a9 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -696,5 +696,6 @@ "optional_name": "可选收件人姓名", "clearnet_link": "明网链接", "onion_link": "洋葱链接", + "settings": "设置", "sell_monero_com_alert_content": "尚不支持出售门罗币" } diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh new file mode 100755 index 000000000..231945659 --- /dev/null +++ b/scripts/macos/app_config.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +CAKEWALLET="cakewallet" +DIR=`pwd` + +if [ -z "$APP_MACOS_TYPE" ]; then + echo "Please set APP_MACOS_TYPE" + exit 1 +fi + +cd ../.. # go to root +cp -rf ./macos/Runner/InfoBase.plist ./macos/Runner/Info.plist +/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${APP_MACOS_BUNDLE_ID}" ./macos/Runner/Info.plist +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${APP_MACOS_VERSION}" ./macos/Runner/Info.plist +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${APP_MACOS_BUILD_NUMBER}" ./macos/Runner/Info.plist + +# Fill entitlements Bundle ID +cp -rf ./macos/Runner/DebugProfileBase.entitlements ./macos/Runner/DebugProfile.entitlements +cp -rf ./macos/Runner/ReleaseBase.entitlements ./macos/Runner/Release.entitlements +sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile.entitlements +sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements +CONFIG_ARGS="" + +case $APP_MACOS_TYPE in + $CAKEWALLET) + CONFIG_ARGS="--monero --bitcoin";; #--haven +esac + +cp -rf pubspec_description.yaml pubspec.yaml +flutter pub get +flutter pub run tool/generate_pubspec.dart +flutter pub get +flutter packages pub run tool/configure.dart $CONFIG_ARGS +cd $DIR \ No newline at end of file diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh new file mode 100755 index 000000000..3fceeb94d --- /dev/null +++ b/scripts/macos/app_env.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +APP_MACOS_NAME="" +APP_MACOS_VERSION="" +APP_MACOS_BUILD_VERSION="" +APP_MACOS_BUNDLE_ID="" + +CAKEWALLET="cakewallet" + +TYPES=($CAKEWALLET) +APP_MACOS_TYPE=$CAKEWALLET + +if [ -n "$1" ]; then + APP_MACOS_TYPE=$1 +fi + +CAKEWALLET_NAME="Cake Wallet" +CAKEWALLET_VERSION="1.0.1" +CAKEWALLET_BUILD_NUMBER=11 +CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" + +if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then + echo "Wrong app type." + exit 1 +fi + +case $APP_MACOS_TYPE in + $CAKEWALLET) + APP_MACOS_NAME=$CAKEWALLET_NAME + APP_MACOS_VERSION=$CAKEWALLET_VERSION + APP_MACOS_BUILD_NUMBER=$CAKEWALLET_BUILD_NUMBER + APP_MACOS_BUNDLE_ID=$CAKEWALLET_BUNDLE_ID;; +esac + +export APP_MACOS_TYPE +export APP_MACOS_NAME +export APP_MACOS_VERSION +export APP_MACOS_BUILD_NUMBER +export APP_MACOS_BUNDLE_ID diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh new file mode 100755 index 000000000..4116704bf --- /dev/null +++ b/scripts/macos/build_all.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./build_monero_all.sh \ No newline at end of file diff --git a/scripts/macos/build_boost_arm64.sh b/scripts/macos/build_boost_arm64.sh new file mode 100755 index 000000000..11f26040f --- /dev/null +++ b/scripts/macos/build_boost_arm64.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_boost_common.sh +build_boost_arm64 \ No newline at end of file diff --git a/scripts/macos/build_boost_common.sh b/scripts/macos/build_boost_common.sh new file mode 100755 index 000000000..0c75be2bd --- /dev/null +++ b/scripts/macos/build_boost_common.sh @@ -0,0 +1,210 @@ +#!/bin/sh + +. ./config.sh + +# Boost combined + +BOOST_CXXFLAGS_COMBINED="-arch x86_64 -arch arm64" +BOOST_CFLAGS_COMBINED="-arch x86_64 -arch arm64" +BOOST_LINKFLAGS_COMBINED="-arch x86_64 -arch arm64" + +# Boost arm64 + +BOOST_CXXFLAGS_ARM64="-arch arm64" +BOOST_CFLAGS_ARM64="-arch arm64" +BOOST_LINKFLAGS_ARM64="-arch arm64" + +# Boost x86_64 + +BOOST_CXXFLAGS_X86_64="-arch x86_64" +BOOST_CFLAGS_X86_64="-arch x86_64" +BOOST_LINKFLAGS_X86_64="-arch x86_64" + +# Boost B2 arm64 + +BOOST_B2_CXXFLAGS_ARM_64="-arch arm64" +BOOST_B2_CFLAGS_ARM_64="-arch arm64" +BOOST_B2_LINKFLAGS_ARM_64="-arch arm64" +BOOST_B2_BUILD_DIR_ARM_64=macos-arm64 + +# Boost B2 x86_64 + +BOOST_B2_CXXFLAGS_X86_64="-arch x86_64" +BOOST_B2_CFLAGS_X86_64="-arch x86_64" +BOOST_B2_LINKFLAGS_X86_64="-arch x86_64" +BOOST_B2_BUILD_DIR_X86_64=macos-x86_64 + +build_boost_init_common() { + CXXFLAGS=$1 + CFLAGS=$2 + LINKFLAGS=$3 + BOOST_SRC_DIR=${EXTERNAL_MACOS_SOURCE_DIR}/boost_1_72_0 + BOOST_FILENAME=boost_1_72_0.tar.bz2 + BOOST_VERSION=1.72.0 + BOOST_FILE_PATH=${EXTERNAL_MACOS_SOURCE_DIR}/${BOOST_FILENAME} + BOOST_SHA256="59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722" + + if [ ! -e "$BOOST_FILE_PATH" ]; then + curl -L http://downloads.sourceforge.net/project/boost/boost/${BOOST_VERSION}/${BOOST_FILENAME} > $BOOST_FILE_PATH + fi + + echo $BOOST_SHA256 *$BOOST_FILE_PATH | shasum -a 256 -c - || exit 1 + + cd $EXTERNAL_MACOS_SOURCE_DIR + rm -rf $BOOST_SRC_DIR + tar -xvf $BOOST_FILE_PATH -C $EXTERNAL_MACOS_SOURCE_DIR + cd $BOOST_SRC_DIR + ./bootstrap.sh --with-toolset=clang-darwin cxxflags="${CXXFLAGS}" cflags="${CFLAGS}" linkflags="${LINKFLAGS}" +} + +build_boost_init_arm64() { + CXXFLAGS="-arch arm64" + CFLAGS="-arch arm64" + LINKFLAGS="-arch arm64" + build_boost_init_common "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" +} + +build_boost_init_x86_64() { + CXXFLAGS="-arch x86_64" + CFLAGS="-arch x86_64" + LINKFLAGS="-arch x86_64" + build_boost_init_common "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" +} + +build_boost_init_universal() { + CXXFLAGS="-arch x86_64 -arch arm64" + CFLAGS="-arch x86_64 -arch arm64" + LINKFLAGS="-arch x86_64 -arch arm64" + build_boost_init_common "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" +} + +build_boost_compile_common() { + ARCH=$1 + ABI=$2 + CXXFLAGS=$3 + CFLAGS=$4 + LINKFLAGS=$5 + FLAGS=$6 + BUILD_DIR=$7 + ./b2 toolset=clang-darwin target-os=darwin architecture="${ARCH}" cxxflags="${CXXFLAGS}" cflags="${CFLAGS}" linkflags="${LINKFLAGS}" abi="${ABI}" "${FLAGS}" -a \ + --with-chrono \ + --with-date_time \ + --with-filesystem \ + --with-program_options \ + --with-regex \ + --with-serialization \ + --with-system \ + --with-thread \ + --with-locale \ + --build-dir=$BUILD_DIR \ + --stagedir=${BUILD_DIR}/stage \ + link=static +} + +build_boost_compile_arm64() { + ARCH="arm" + ABI="aapcs" + CXXFLAGS="-arch arm64" + CFLAGS="-arch arm64" + LINKFLAGS="-arch arm64" + FLAGS="" + BUILD_DIR="macos-arm64" + build_boost_compile_common "${ARCH}" "${ABI}" "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" "${FLAGS}" "${BUILD_DIR}" +} + +build_boost_compile_x86_64() { + ARCH="x86" + ABI="sysv" + CXXFLAGS="-arch x86_64" + CFLAGS="-arch x86_64" + LINKFLAGS="-arch x86_64" + FLAGS="binary-format=mach-o" + BUILD_DIR="macos-x86_64" + build_boost_compile_common "${ARCH}" "${ABI}" "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" "${FLAGS}" "${BUILD_DIR}" +} + +build_boost_compile_universal() { + ARCHES=(arm x86) + for ARCH in ${ARCHES[@]}; do + ABI="" + CXXFLAGS="" + CFLAGS="" + LINKFLAGS="" + FLAGS="" + BUILD_DIR="" + + case $ARCH in + arm) + ABI="aapcs" + CXXFLAGS="-arch arm64" + CFLAGS="-arch arm64" + LINKFLAGS="-arch arm64" + FLAGS="" + BUILD_DIR="macos-arm64";; + x86) + ABI="sysv" + CXXFLAGS="-arch x86_64" + CFLAGS="-arch x86_64" + LINKFLAGS="-arch x86_64" + FLAGS="binary-format=mach-o" + BUILD_DIR="macos-x86_64" + esac + + build_boost_compile_common "${ARCH}" "${ABI}" "${CXXFLAGS}" "${CFLAGS}" "${LINKFLAGS}" "${FLAGS}" "${BUILD_DIR}" + done +} + +build_boost_install_common() { + ARCH=$1 + LIB_DIR="" + mkdir $EXTERNAL_MACOS_LIB_DIR + mkdir $EXTERNAL_MACOS_INCLUDE_DIR + + case $ARCH in + arm64) LIB_DIR="${BOOST_B2_BUILD_DIR_ARM_64}/stage/lib";; + x86_64) LIB_DIR="${BOOST_B2_BUILD_DIR_X86_64}/stage/lib";; + *) LIB_DIR="lib";; + esac + + cp -r ${LIB_DIR}/*.a ${EXTERNAL_MACOS_LIB_DIR} + cp -r boost ${EXTERNAL_MACOS_INCLUDE_DIR} +} + +build_boost_install_arm64() { + ARCH="arm64" + build_boost_install_common $ARCH +} + +build_boost_install_x86_64() { + ARCH="x86_64" + build_boost_install_common $ARCH +} + +build_boost_install_universal() { + mkdir lib + + for blib in ${BOOST_B2_BUILD_DIR_ARM_64}/stage/lib/*.a; do + lipo -create -arch arm64 $blib -arch x86_64 ${BOOST_B2_BUILD_DIR_X86_64}/stage/lib/$(basename $blib) -output lib/$(basename $blib); + done + + cp -r lib/* ${EXTERNAL_MACOS_LIB_DIR} + cp -r boost ${EXTERNAL_MACOS_INCLUDE_DIR} +} + +build_boost_arm64() { + build_boost_init_arm64 + build_boost_compile_arm64 + build_boost_install_arm64 +} + +build_boost_x86_64() { + build_boost_init_x86_64 + build_boost_compile_x86_64 + build_boost_install_x86_64 +} + +build_boost_universal() { + build_boost_init_universal + build_boost_compile_universal + build_boost_install_universal +} \ No newline at end of file diff --git a/scripts/macos/build_boost_universal.sh b/scripts/macos/build_boost_universal.sh new file mode 100755 index 000000000..0b5945aec --- /dev/null +++ b/scripts/macos/build_boost_universal.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_boost_common.sh +build_boost_universal \ No newline at end of file diff --git a/scripts/macos/build_boost_x86_64.sh b/scripts/macos/build_boost_x86_64.sh new file mode 100755 index 000000000..a697b7027 --- /dev/null +++ b/scripts/macos/build_boost_x86_64.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_boost_common.sh +build_boost_x86_64 \ No newline at end of file diff --git a/scripts/macos/build_expat.sh b/scripts/macos/build_expat.sh new file mode 100755 index 000000000..0c5857907 --- /dev/null +++ b/scripts/macos/build_expat.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +. ./config.sh + +EXPAT_VERSION=R_2_4_8 +EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" +EXPAT_SRC_DIR=${EXTERNAL_MACOS_SOURCE_DIR}/libexpat + +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 + +./buildconf.sh +./configure --enable-static --disable-shared --prefix=${EXTERNAL_MACOS_DIR} +make +make install \ No newline at end of file diff --git a/scripts/macos/build_haven.sh b/scripts/macos/build_haven.sh new file mode 100755 index 000000000..fb67da442 --- /dev/null +++ b/scripts/macos/build_haven.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +. ./config.sh + +HAVEN_URL="https://github.com/haven-protocol-org/haven-main.git" +HAVEN_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/haven" +HAVEN_VERSION=tags/v3.0.0 +BUILD_TYPE=release +PREFIX=${EXTERNAL_MACOS_DIR} +DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/haven +DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/haven +ARCH=`uname -m` + +echo "Cloning haven from - $HAVEN_URL to - $HAVEN_DIR_PATH" +git clone $HAVEN_URL $HAVEN_DIR_PATH +cd $HAVEN_DIR_PATH +git checkout $HAVEN_VERSION +git submodule update --init --force +mkdir -p build +cd .. + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +if [ -z $INSTALL_PREFIX ]; then + INSTALL_PREFIX=${ROOT_DIR}/haven +fi + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +echo "Building MACOS ${ARCH}" +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" +rm -rf haven/build > /dev/null + +mkdir -p haven/build/${BUILD_TYPE} +pushd haven/build/${BUILD_TYPE} +cmake -DARCH=${ARCH} \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DSTATIC=ON \ + -DBUILD_GUI_DEPS=ON \ + -DINSTALL_VENDORED_LIBUNBOUND=ON \ + -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -DUSE_DEVICE_TREZOR=OFF \ + ../.. +make -j4 && make install +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR +popd + diff --git a/scripts/macos/build_monero.sh b/scripts/macos/build_monero.sh new file mode 100755 index 000000000..4dc9a9137 --- /dev/null +++ b/scripts/macos/build_monero.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +. ./config.sh + +MONERO_URL="https://github.com/cake-tech/monero.git" +MONERO_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/monero" +MONERO_VERSION=release-v0.18.0.0 +BUILD_TYPE=release +PREFIX=${EXTERNAL_MACOS_DIR} +DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/monero +DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/monero +ARCH=`uname -m` + +echo "Cloning monero from - $MONERO_URL to - $MONERO_DIR_PATH" +git clone $MONERO_URL $MONERO_DIR_PATH +cd $MONERO_DIR_PATH +git checkout $MONERO_VERSION +git submodule update --init --force +mkdir -p build +cd .. + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +if [ -z $INSTALL_PREFIX ]; then + INSTALL_PREFIX=${ROOT_DIR}/monero +fi + +echo "Building MACOS ${ARCH}" +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" +rm -r monero/build > /dev/null + +if [ "${ARCH}" == "x86_64" ]; then + ARCH="x86-64" +fi + +mkdir -p monero/build/${BUILD_TYPE} +pushd monero/build/${BUILD_TYPE} +cmake -DARCH=${ARCH} \ + -DBUILD_64=ON \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DSTATIC=ON \ + -DBUILD_GUI_DEPS=ON \ + -DUNBOUND_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR} \ + -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -DUSE_DEVICE_TREZOR=OFF \ + ../.. +make wallet_api -j4 +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR +popd diff --git a/scripts/macos/build_monero_all.sh b/scripts/macos/build_monero_all.sh new file mode 100755 index 000000000..f7e55909b --- /dev/null +++ b/scripts/macos/build_monero_all.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +ARCH=`uname -m` + +. ./config.sh + +case $ARCH in + arm64) + ./build_openssl_arm64.sh + ./build_boost_arm64.sh;; + x86_64) + ./build_openssl_x86_64.sh + ./build_boost_x86_64.sh;; +esac + +./build_zmq.sh +./build_expat.sh +./build_unbound.sh +./build_sodium.sh +./build_monero.sh \ No newline at end of file diff --git a/scripts/macos/build_openssl_arm64.sh b/scripts/macos/build_openssl_arm64.sh new file mode 100755 index 000000000..fd8d7b2f5 --- /dev/null +++ b/scripts/macos/build_openssl_arm64.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_openssl_common.sh +build_openssl_arm64 \ No newline at end of file diff --git a/scripts/macos/build_openssl_common.sh b/scripts/macos/build_openssl_common.sh new file mode 100755 index 000000000..fd07312fa --- /dev/null +++ b/scripts/macos/build_openssl_common.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +. ./config.sh + +OPEN_SSL_DIR_NAME="OpenSSL" +OPEN_SSL_x86_64_DIR_NAME="${OPEN_SSL_DIR_NAME}-x86_64" +OPEN_SSL_ARM_DIR_NAME="${OPEN_SSL_DIR_NAME}-arm" +OPEN_SSL_X86_64_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/${OPEN_SSL_x86_64_DIR_NAME}" +OPEN_SSL_ARM_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/${OPEN_SSL_ARM_DIR_NAME}" + +build_openssl_init_common() { + DIR=$1 + # Use 1.1.1s becasue of https://github.com/openssl/openssl/issues/18720 + OPENSSL_VERSION="1.1.1s" + + echo "============================ OpenSSL ============================" + + cd $EXTERNAL_MACOS_SOURCE_DIR + curl -O https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz + tar -xvzf openssl-$OPENSSL_VERSION.tar.gz + rm -rf $DIR + rm -rf $OPEN_SSL_DIR_PATH + mv openssl-$OPENSSL_VERSION $DIR + tar -xvzf openssl-$OPENSSL_VERSION.tar.gz + mv openssl-$OPENSSL_VERSION $OPEN_SSL_ARM_DIR_NAME +} + +build_openssl_init_arm64() { + DIR=$OPEN_SSL_ARM_DIR_PATH + build_openssl_init_common ${DIR} +} + +build_openssl_init_x86_64() { + DIR=$OPEN_SSL_X86_64_DIR_PATH + build_openssl_init_common ${DIR} +} + +build_openssl_compile_common() { + ARCH=$1 + DIR="" + XARCH="" + case $ARCH in + arm64) + DIR=$OPEN_SSL_ARM_DIR_PATH + XARCH="darwin64-arm64-cc";; + x86_64) + DIR=$OPEN_SSL_X86_64_DIR_PATH + XARCH="darwin64-x86_64-cc";; + esac + + echo "Build OpenSSL for ${ARCH}" + cd $DIR + ./Configure $XARCH + make + +} + +build_openssl_compile_arm64() { + ARCH=arm64 + build_openssl_compile_common "${ARCH}" +} + +build_openssl_compile_x86_64() { + ARCH=x86_64 + build_openssl_compile_common "${ARCH}" +} + +build_openssl_install_common() { + DIR=$1 + mv ${DIR}/include/* $EXTERNAL_MACOS_INCLUDE_DIR + mv ${DIR}/libcrypto.a ${EXTERNAL_MACOS_LIB_DIR}/libcrypto.a + mv ${DIR}/libssl.a ${EXTERNAL_MACOS_LIB_DIR}/libssl.a +} + +build_openssl_install_arm64() { + build_openssl_install_common "${OPEN_SSL_ARM_DIR_PATH}" +} + +build_openssl_install_x86_64() { + build_openssl_install_common "${OPEN_SSL_X86_64_DIR_PATH}" +} + +build_openssl_install_universal() { + OPEN_SSL_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/${OPEN_SSL_DIR_NAME}" + mv ${OPEN_SSL_ARM_DIR_PATH}/include/* $OPEN_SSL_DIR_PATH/include + build_openssl_install_common "${OPEN_SSL_DIR_PATH}" +} + +build_openssl_arm64() { + build_openssl_init_arm64 + build_openssl_compile_arm64 + build_openssl_install_arm64 +} + +build_openssl_x86_64() { + build_openssl_init_x86_64 + build_openssl_compile_x86_64 + build_openssl_install_x86_64 +} + +build_openssl_combine() { + OPEN_SSL_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/${OPEN_SSL_DIR_NAME}" + echo "Create universal bin" + mkdir -p $OPEN_SSL_DIR_PATH/include + lipo -create ${OPEN_SSL_ARM_DIR_PATH}/libcrypto.a ${OPEN_SSL_X86_64_DIR_PATH}/libcrypto.a -output ${OPEN_SSL_DIR_PATH}/libcrypto.a + lipo -create ${OPEN_SSL_ARM_DIR_PATH}/libssl.a ${OPEN_SSL_X86_64_DIR_PATH}/libssl.a -output ${OPEN_SSL_DIR_PATH}/libssl.a +} + +build_openssl_universal() { + build_openssl_init_arm64 + build_openssl_compile_arm64 + build_openssl_init_x86_64 + build_openssl_compile_x86_64 + build_openssl_combine + build_openssl_install_universal +} \ No newline at end of file diff --git a/scripts/macos/build_openssl_universal.sh b/scripts/macos/build_openssl_universal.sh new file mode 100755 index 000000000..ba7fc5e4a --- /dev/null +++ b/scripts/macos/build_openssl_universal.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_openssl_common.sh +build_openssl_universal \ No newline at end of file diff --git a/scripts/macos/build_openssl_x86_64.sh b/scripts/macos/build_openssl_x86_64.sh new file mode 100755 index 000000000..6ef326e8a --- /dev/null +++ b/scripts/macos/build_openssl_x86_64.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./build_openssl_common.sh +build_openssl_x86_64 \ No newline at end of file diff --git a/scripts/macos/build_sodium.sh b/scripts/macos/build_sodium.sh new file mode 100755 index 000000000..b50d3c2ee --- /dev/null +++ b/scripts/macos/build_sodium.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +. ./config.sh + +SODIUM_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libsodium" +SODIUM_URL="https://github.com/jedisct1/libsodium.git" + +echo "============================ SODIUM ============================" + +echo "Cloning SODIUM from - $SODIUM_URL" +git clone $SODIUM_URL $SODIUM_PATH --branch stable +cd $SODIUM_PATH +./dist-build/osx.sh + +mv ${SODIUM_PATH}/libsodium-osx/include/* $EXTERNAL_MACOS_INCLUDE_DIR +mv ${SODIUM_PATH}/libsodium-osx/lib/* $EXTERNAL_MACOS_LIB_DIR \ No newline at end of file diff --git a/scripts/macos/build_unbound.sh b/scripts/macos/build_unbound.sh new file mode 100755 index 000000000..ed115d464 --- /dev/null +++ b/scripts/macos/build_unbound.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +. ./config.sh + +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_MACOS_SOURCE_DIR}/unbound-1.16.2" + +echo "============================ Unbound ============================" +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 + +./configure --prefix="${EXTERNAL_MACOS_DIR}" \ + --with-ssl="${EXTERNAL_MACOS_DIR}" \ + --with-libexpat="${EXTERNAL_MACOS_DIR}" \ + --enable-static \ + --disable-shared \ + --disable-flto +make +make install \ No newline at end of file diff --git a/scripts/macos/build_zmq.sh b/scripts/macos/build_zmq.sh new file mode 100755 index 000000000..dd5623f06 --- /dev/null +++ b/scripts/macos/build_zmq.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +. ./config.sh + +ZMQ_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libzmq" +ZMQ_URL="https://github.com/zeromq/libzmq.git" + +echo "============================ ZMQ ============================" + +echo "Cloning ZMQ from - $ZMQ_URL" +git clone $ZMQ_URL $ZMQ_PATH +cd $ZMQ_PATH +mkdir cmake-build +cd cmake-build +cmake .. -DCMAKE_INSTALL_PREFIX="${EXTERNAL_MACOS_DIR}" +make +make install \ No newline at end of file diff --git a/scripts/macos/cakewallet.sh b/scripts/macos/cakewallet.sh new file mode 100755 index 000000000..5f591327e --- /dev/null +++ b/scripts/macos/cakewallet.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./app_env.sh "cakewallet" +. ./app_config.sh \ No newline at end of file diff --git a/scripts/macos/config.sh b/scripts/macos/config.sh new file mode 100755 index 000000000..493aaa6c3 --- /dev/null +++ b/scripts/macos/config.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +export MACOS_SCRIPTS_DIR=`pwd` +export CW_ROOT=${MACOS_SCRIPTS_DIR}/../.. +export EXTERNAL_DIR=${CW_ROOT}/cw_shared_external/ios/External +export EXTERNAL_MACOS_DIR=${EXTERNAL_DIR}/macos +export EXTERNAL_MACOS_SOURCE_DIR=${EXTERNAL_MACOS_DIR}/sources +export EXTERNAL_MACOS_LIB_DIR=${EXTERNAL_MACOS_DIR}/lib +export EXTERNAL_MACOS_INCLUDE_DIR=${EXTERNAL_MACOS_DIR}/include + +mkdir -p $EXTERNAL_MACOS_LIB_DIR +mkdir -p $EXTERNAL_MACOS_INCLUDE_DIR +mkdir -p $EXTERNAL_MACOS_SOURCE_DIR \ No newline at end of file diff --git a/scripts/macos/gen_arm64.sh b/scripts/macos/gen_arm64.sh new file mode 100755 index 000000000..ca604bb43 --- /dev/null +++ b/scripts/macos/gen_arm64.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. ./gen_common.sh + +gen "arm64" \ No newline at end of file diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh new file mode 100755 index 000000000..62f4effab --- /dev/null +++ b/scripts/macos/gen_common.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +gen_podspec() { + ARCH=$1 + CW_PLUGIN_DIR="`pwd`/../../cw_monero/macos" + BASE_FILENAME="cw_monero_base.podspec" + BASE_FILE_PATH="${CW_PLUGIN_DIR}/${BASE_FILENAME}" + DEFAULT_FILENAME="cw_monero.podspec" + DEFAULT_FILE_PATH="${CW_PLUGIN_DIR}/${DEFAULT_FILENAME}" + rm -f $DEFAULT_FILE_PATH + cp $BASE_FILE_PATH $DEFAULT_FILE_PATH + sed -i '' "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH +} + +gen_project() { + ARCH=$1 + CW_DIR="`pwd`/../../macos/Runner.xcodeproj" + BASE_FILENAME="project_base.pbxproj" + BASE_FILE_PATH="${CW_DIR}/${BASE_FILENAME}" + DEFAULT_FILENAME="project.pbxproj" + DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" + rm -f $DEFAULT_FILE_PATH + cp $BASE_FILE_PATH $DEFAULT_FILE_PATH + sed -i '' "s/ARCHS =.*/ARCHS = ${ARCH};/g" $DEFAULT_FILE_PATH +} + +gen() { + ARCH=$1 + gen_podspec "${ARCH}" + gen_project "${ARCH}" +} \ No newline at end of file diff --git a/scripts/macos/gen_universal.sh b/scripts/macos/gen_universal.sh new file mode 100755 index 000000000..6056053a5 --- /dev/null +++ b/scripts/macos/gen_universal.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. ./gen_common.sh + +gen "arm64, x86_64" \ No newline at end of file diff --git a/scripts/macos/gen_x86_64.sh b/scripts/macos/gen_x86_64.sh new file mode 100755 index 000000000..c6988d8f0 --- /dev/null +++ b/scripts/macos/gen_x86_64.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. ./gen_common.sh + +gen "x86_64" \ No newline at end of file diff --git a/scripts/macos/setup.sh b/scripts/macos/setup.sh new file mode 100755 index 000000000..fea2a6c8d --- /dev/null +++ b/scripts/macos/setup.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +. ./config.sh + +cd $EXTERNAL_MACOS_LIB_DIR + + +# LIBRANDOMX_PATH=${EXTERNAL_MACOS_LIB_DIR}/monero/librandomx.a + +# if [ -f "$LIBRANDOMX_PATH" ]; then +# cp $LIBRANDOMX_PATH ./haven +# fi + +libtool -static -o libboost.a ./libboost_*.a +libtool -static -o libmonero.a ./monero/*.a + +# CW_HAVEN_EXTERNAL_LIB=../../../../../cw_haven/macos/External/macos/lib +# CW_HAVEN_EXTERNAL_INCLUDE=../../../../../cw_haven/macos/External/macos/include +CW_MONERO_EXTERNAL_LIB=../../../../../cw_monero/macos/External/macos/lib +CW_MONERO_EXTERNAL_INCLUDE=../../../../../cw_monero/macos/External/macos/include + +# mkdir -p $CW_HAVEN_EXTERNAL_INCLUDE +mkdir -p $CW_MONERO_EXTERNAL_INCLUDE +# mkdir -p $CW_HAVEN_EXTERNAL_LIB +mkdir -p $CW_MONERO_EXTERNAL_LIB + +# ln ./libboost.a ${CW_HAVEN_EXTERNAL_LIB}/libboost.a +# ln ./libcrypto.a ${CW_HAVEN_EXTERNAL_LIB}/libcrypto.a +# ln ./libssl.a ${CW_HAVEN_EXTERNAL_LIB}/libssl.a +# ln ./libsodium.a ${CW_HAVEN_EXTERNAL_LIB}/libsodium.a +# cp ./libhaven.a $CW_HAVEN_EXTERNAL_LIB +# cp ../include/haven/* $CW_HAVEN_EXTERNAL_INCLUDE + +ln ./libboost.a ${CW_MONERO_EXTERNAL_LIB}/libboost.a +ln ./libcrypto.a ${CW_MONERO_EXTERNAL_LIB}/libcrypto.a +ln ./libssl.a ${CW_MONERO_EXTERNAL_LIB}/libssl.a +ln ./libsodium.a ${CW_MONERO_EXTERNAL_LIB}/libsodium.a +ln ./libunbound.a ${CW_MONERO_EXTERNAL_LIB}/libunbound.a +cp ./libmonero.a $CW_MONERO_EXTERNAL_LIB +cp ../include/monero/* $CW_MONERO_EXTERNAL_INCLUDE \ No newline at end of file diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index e4e59819f..621ab1cfc 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -13,6 +13,7 @@ class SecretKey { SecretKey('backupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(8).bytes)), SecretKey('backupKeychainSalt', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), SecretKey('changeNowApiKey', () => ''), + SecretKey('changeNowApiKeyDesktop', () => ''), SecretKey('wyreSecretKey', () => ''), SecretKey('wyreApiKey', () => ''), SecretKey('wyreAccountId', () => ''), @@ -21,6 +22,7 @@ class SecretKey { SecretKey('sideShiftAffiliateId', () => ''), SecretKey('sideShiftApiKey', () => ''), SecretKey('simpleSwapApiKey', () => ''), + SecretKey('simpleSwapApiKeyDesktop', () => ''), SecretKey('anypayToken', () => ''), SecretKey('onramperApiKey', () => ''), SecretKey('ioniaClientId', () => ''), From ff7a217d8417eee05f6942198bb23bef031d96a9 Mon Sep 17 00:00:00 2001 From: Rafael Saes <76502841+saltrafael@users.noreply.github.com> Date: Fri, 14 Apr 2023 11:52:07 -0300 Subject: [PATCH 07/28] CW-338: Currency picker UI when keyboard is showing (#854) * fix: Currency picker UI when keyboard is showing * refactor: move picker logic into the common Picker widget - CurrencyPicker uses the common Picker widget in grid mode - SeedLanguagePicker uses the common Picker widget in grid mode - Added logic for keyboard showing UI into Picker widget - Added `softWrap: true` to the item text, so it doesn't overflow * fix: remove subPickerItemsList * fix: add final * fix: move function out of initState() * fix: keep build functions separate to remove boolean comparisons * fix: remove onItemSelected from already selected item * fix: change Expanded for Flexible widget --- .../exchange/widgets/currency_picker.dart | 162 +----- .../wallet_restore_from_seed_form.dart | 12 +- .../widgets/seed_language_picker.dart | 151 +----- lib/src/widgets/alert_close_button.dart | 9 +- lib/src/widgets/picker.dart | 507 ++++++++++++------ lib/src/widgets/seed_language_selector.dart | 26 +- 6 files changed, 429 insertions(+), 438 deletions(-) diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 5ed9c6f7d..0fe1d4e67 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -1,13 +1,8 @@ -import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker_item_widget.dart'; -import 'package:cake_wallet/src/screens/exchange/widgets/picker_item.dart'; -import 'package:cake_wallet/src/widgets/alert_close_button.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cw_core/currency.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/picker_item.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/src/widgets/alert_background.dart'; -import 'package:cake_wallet/palette.dart'; -import 'currency_picker_widget.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cw_core/currency.dart'; class CurrencyPicker extends StatefulWidget { CurrencyPicker( @@ -19,7 +14,7 @@ class CurrencyPicker extends StatefulWidget { this.isMoneroWallet = false, this.isConvertFrom = false}); - int selectedAtIndex; + final int selectedAtIndex; final List<Currency> items; final String? title; final Function(Currency) onItemSelected; @@ -35,144 +30,39 @@ class CurrencyPickerState extends State<CurrencyPicker> { CurrencyPickerState(this.items) : isSearchBarActive = false, textFieldValue = '', - subPickerItemsList = items, - appBarTextStyle = - TextStyle(fontSize: 20, fontFamily: 'Lato', backgroundColor: Colors.transparent, color: Colors.white), - pickerItemsList = <PickerItem<Currency>>[]; + appBarTextStyle = TextStyle( + fontSize: 20, + fontFamily: 'Lato', + backgroundColor: Colors.transparent, + color: Colors.white), + pickerItemsList = <PickerItem<CryptoCurrency>>[]; List<PickerItem<Currency>> pickerItemsList; List<Currency> items; bool isSearchBarActive; String textFieldValue; - List<Currency> subPickerItemsList; TextStyle appBarTextStyle; - void cleanSubPickerItemsList() => subPickerItemsList = items; - - void currencySearchBySubstring(String subString) { - setState(() { - if (subString.isNotEmpty) { - subPickerItemsList = items - .where((element) => - element.name.toLowerCase().contains(subString.toLowerCase()) || - (element.tag != null ? element.tag!.toLowerCase().contains(subString.toLowerCase()) : false) || - (element.fullName != null ? element.fullName!.toLowerCase().contains(subString.toLowerCase()) : false)) - .toList(); - return; - } - cleanSubPickerItemsList(); - }); + bool currencySearchBySubstring(Currency currency, String subString) { + return currency.name.toLowerCase().contains(subString.toLowerCase()) || + (currency.tag != null + ? currency.tag!.toLowerCase().contains(subString.toLowerCase()) + : false) || + (currency.fullName != null + ? currency.fullName!.toLowerCase().contains(subString.toLowerCase()) + : false); } @override Widget build(BuildContext context) { - return AlertBackground( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - 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.headline6!.color!, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.65, - maxWidth: ResponsiveLayoutUtil.kPopupWidth - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.hintText != null) - Padding( - padding: const EdgeInsets.all(16), - child: TextFormField( - style: TextStyle(color: Palette.darkBlueCraiola), - decoration: InputDecoration( - hintText: widget.hintText, - prefixIcon: Image.asset("assets/images/search_icon.png"), - filled: true, - fillColor: const Color(0xffF2F0FA), - alignLabelWithHint: false, - contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - ), - onChanged: (value) { - this.textFieldValue = value; - cleanSubPickerItemsList(); - currencySearchBySubstring(textFieldValue); - }, - ), - ), - Divider( - color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, - height: 1, - ), - if (widget.selectedAtIndex != -1) - AspectRatio( - aspectRatio: 6, - child: PickerItemWidget( - title: items[widget.selectedAtIndex].name, - iconPath: items[widget.selectedAtIndex].iconPath, - isSelected: true, - tag: items[widget.selectedAtIndex].tag, - ), - ), - Flexible( - child: CurrencyPickerWidget( - crossAxisCount: 2, - selectedAtIndex: widget.selectedAtIndex, - pickerItemsList: subPickerItemsList, - pickListItem: (int index) { - setState(() { - widget.selectedAtIndex = index; - }); - widget.onItemSelected(subPickerItemsList[index]); - if (widget.isConvertFrom && - !widget.isMoneroWallet && - (subPickerItemsList[index] == CryptoCurrency.xmr)) { - } else { - Navigator.of(context).pop(); - } - }, - ), - ), - ], - ), - ), - ), - ), - ), - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), - AlertCloseButton(), - ], - ), + return Picker( + selectedAtIndex: widget.selectedAtIndex, + items: items, + isGridView: true, + title: widget.title, + hintText: widget.hintText, + matchingCriteria: currencySearchBySubstring, + onItemSelected: widget.onItemSelected, ); } } 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 5849eb2e6..3071d9a82 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -113,16 +113,10 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> { if (widget.displayLanguageSelector) GestureDetector( onTap: () async { - final selected = await showPopUp<String>( + await showPopUp<void>( context: context, - builder: (BuildContext context) => - SeedLanguagePicker(selected: language)); - - if (selected == null || selected.isEmpty) { - return; - } - - _changeLanguage(selected); + builder: (_) => SeedLanguagePicker( + selected: language, onItemSelected: _changeLanguage)); }, child: Container( color: Colors.transparent, diff --git a/lib/src/screens/seed_language/widgets/seed_language_picker.dart b/lib/src/screens/seed_language/widgets/seed_language_picker.dart index 64e050149..0e1e63f57 100644 --- a/lib/src/screens/seed_language/widgets/seed_language_picker.dart +++ b/lib/src/screens/seed_language/widgets/seed_language_picker.dart @@ -1,9 +1,6 @@ -import 'dart:ui'; -import 'package:cake_wallet/src/widgets/alert_background.dart'; -import 'package:cake_wallet/src/widgets/alert_close_button.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; List<Image> flagImages = [ @@ -50,138 +47,40 @@ const List<String> seedLanguages = [ enum Places { topLeft, topRight, bottomLeft, bottomRight, inside } class SeedLanguagePicker extends StatefulWidget { - SeedLanguagePicker({ - Key? key, - this.selected = defaultSeedLanguage}) + SeedLanguagePicker( + {Key? key, + this.selected = defaultSeedLanguage, + required this.onItemSelected}) : super(key: key); final String selected; + final Function(String) onItemSelected; @override - SeedLanguagePickerState createState() => - SeedLanguagePickerState(selected: selected); + SeedLanguagePickerState createState() => SeedLanguagePickerState( + selected: selected, onItemSelected: onItemSelected); } class SeedLanguagePickerState extends State<SeedLanguagePicker> { - SeedLanguagePickerState({required this.selected}); + SeedLanguagePickerState( + {required this.selected, required this.onItemSelected}); - final closeButton = Image.asset('assets/images/close.png'); - String selected; + final String selected; + final Function(String) onItemSelected; @override Widget build(BuildContext context) { - return AlertBackground( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: Text( - S.of(context).seed_choose, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - fontFamily: 'Lato', - decoration: TextDecoration.none, - color: Colors.white), - ), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(14)), - child: Container( - height: 300, - width: 300, - color: - Theme.of(context).accentTextTheme!.headline6!.backgroundColor!, - child: GridView.count( - padding: EdgeInsets.all(0), - shrinkWrap: true, - crossAxisCount: 3, - childAspectRatio: 4 / 3, - physics: const NeverScrollableScrollPhysics(), - crossAxisSpacing: 1, - mainAxisSpacing: 1, - children: List.generate(11, (index) { - if (index == 10) { - return gridTile( - isCurrent: false, - image: null, - text: '', - onTap: () {}); - } - - final code = languageCodes[index]; - final flag = flagImages[index]; - final isCurrent = - index == seedLanguages.indexOf(selected); - - return gridTile( - isCurrent: isCurrent, - image: flag, - text: code, - onTap: () { - selected = seedLanguages[index]; - Navigator.of(context).pop(selected); - }); - }), - ), - ), - ), - ) - ], - ), - AlertCloseButton(image: closeButton) - ], - )); - } - - Widget gridTile( - {required bool isCurrent, - required String text, - required VoidCallback onTap, - Image? image}) { - final color = isCurrent - ? Theme.of(context).textTheme!.bodyText1!.color! - : Theme.of(context).accentTextTheme!.headline6!.color!; - final textColor = isCurrent - ? Palette.blueCraiola - : Theme.of(context).primaryTextTheme!.headline6!.color!; - - return GestureDetector( - onTap: onTap, - child: Container( - padding: EdgeInsets.all(10), - color: color, - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - image ?? Offstage(), - Padding( - padding: image != null - ? EdgeInsets.only(left: 10) - : EdgeInsets.only(left: 0), - child: Text( - text, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - decoration: TextDecoration.none, - color: textColor), - ), - ) - ], - ), - ), - )); + return Picker( + selectedAtIndex: seedLanguages.indexOf(selected), + items: seedLanguages, + images: flagImages, + isGridView: true, + title: S.of(context).seed_choose, + hintText: S.of(context).seed_choose, + matchingCriteria: (String language, String searchText) { + return language.toLowerCase().contains(searchText); + }, + onItemSelected: onItemSelected, + ); } } diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index e8e20f125..1aa8277f3 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -2,9 +2,10 @@ import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; class AlertCloseButton extends StatelessWidget { - AlertCloseButton({this.image}); + AlertCloseButton({this.image, this.bottom}); final Image? image; + final double? bottom; final closeButton = Image.asset( 'assets/images/close.png', @@ -13,7 +14,9 @@ class AlertCloseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( + return Positioned( + bottom: bottom ?? 60, + child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( height: 42, @@ -26,6 +29,6 @@ class AlertCloseButton extends StatelessWidget { child: image ?? closeButton, ), ), - ); + )); } } \ No newline at end of file diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index ccf922d41..34ff10316 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.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:cw_core/currency.dart'; class Picker<Item> extends StatefulWidget { Picker({ @@ -37,7 +38,8 @@ class Picker<Item> extends StatefulWidget { final bool Function(Item, String)? matchingCriteria; @override - _PickerState<Item> createState() => _PickerState<Item>(items, images, onItemSelected); + _PickerState<Item> createState() => + _PickerState<Item>(items, images, onItemSelected); } class _PickerState<Item> extends State<Picker<Item>> { @@ -46,132 +48,235 @@ class _PickerState<Item> extends State<Picker<Item>> { final Function(Item) onItemSelected; List<Item> items; List<Image> images; + List<Item> filteredItems = []; + List<Image> filteredImages = []; final TextEditingController searchController = TextEditingController(); ScrollController controller = ScrollController(); + void clearFilteredItemsList() { + filteredItems = List.from( + items, + growable: true, + ); + filteredImages = List.from( + images, + growable: true, + ); + + if (widget.selectedAtIndex != -1) { + if (widget.selectedAtIndex < filteredItems.length) { + filteredItems.removeAt(widget.selectedAtIndex); + } + + if (widget.selectedAtIndex < filteredImages.length) { + filteredImages.removeAt(widget.selectedAtIndex); + } + } + } + @override void initState() { super.initState(); + clearFilteredItemsList(); + searchController.addListener(() { - items = []; - images = []; - for (int i=0;i<widget.items.length;i++) { - if (widget.matchingCriteria?.call(widget.items[i], searchController.text) ?? true) { - items.add(widget.items[i]); - images.add(widget.images[i]); - } - } - setState(() {}); + clearFilteredItemsList(); + + setState(() { + filteredItems = List.from(items.where((element) { + if (widget.selectedAtIndex != items.indexOf(element) && + (widget.matchingCriteria?.call(element, searchController.text) ?? + true)) { + if (images.isNotEmpty) { + filteredImages.add(images[items.indexOf(element)]); + } + return true; + } + + if (filteredImages.isNotEmpty) { + filteredImages.remove(images[items.indexOf(element)]); + } + return false; + }), growable: true); + + return; + }); }); } @override Widget build(BuildContext context) { + final double padding = 24; + + final mq = MediaQuery.of(context); + final bottom = mq.viewInsets.bottom; + final height = mq.size.height - bottom; + final screenCenter = height / 2; + + double closeButtonBottom = 60; + double containerHeight = height * 0.65; + if (bottom > 0) { + // increase a bit or it gets too squished in the top + containerHeight = height * 0.75; + + final containerCenter = containerHeight / 2; + final containerBottom = screenCenter - containerCenter; + + final hasTitle = widget.title == null || widget.title!.isEmpty; + + // position the close button right below the search container + closeButtonBottom = closeButtonBottom - + containerBottom + + (hasTitle ? padding : padding / 1.5); + } + return AlertBackground( child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - 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.headline6!.color!, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.65, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.hintText != null) - Padding( - padding: const EdgeInsets.all(16), - child: TextFormField( - controller: searchController, - style: TextStyle(color: Theme.of(context).primaryTextTheme.headline6!.color!), - decoration: InputDecoration( - hintText: widget.hintText, - prefixIcon: Image.asset("assets/images/search_icon.png"), - filled: true, - fillColor: Theme.of(context).accentTextTheme.headline3!.color!, - alignLabelWithHint: false, - contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: Colors.transparent, - )), - ), + children: [ + Expanded( + flex: 1, + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + if (widget.title?.isNotEmpty ?? false) + Container( + padding: EdgeInsets.symmetric(horizontal: padding), + 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.symmetric(horizontal: padding), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + color: Theme.of(context) + .accentTextTheme + .headline6! + .color!, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: containerHeight, + maxWidth: ResponsiveLayoutUtil.kPopupWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.hintText != null) + Padding( + padding: const EdgeInsets.all(16), + child: TextFormField( + controller: searchController, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline6! + .color!), + decoration: InputDecoration( + hintText: widget.hintText, + prefixIcon: Image.asset( + "assets/images/search_icon.png"), + filled: true, + fillColor: Theme.of(context) + .accentTextTheme + .headline3! + .color!, + alignLabelWithHint: false, + contentPadding: + const EdgeInsets.symmetric( + vertical: 4, horizontal: 16), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.transparent, + )), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.transparent, + )), + ), + ), + ), + Divider( + color: Theme.of(context) + .accentTextTheme + .headline6! + .backgroundColor!, + height: 1, + ), + if (widget.selectedAtIndex != -1) + buildSelectedItem(widget.selectedAtIndex), + Flexible( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + filteredItems.length > 3 + ? Scrollbar( + controller: controller, + child: itemsList(), + ) + : itemsList(), + (widget.description?.isNotEmpty ?? false) + ? Positioned( + bottom: padding, + left: padding, + right: padding, + child: Text( + widget.description!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + decoration: + TextDecoration.none, + color: Theme.of(context) + .primaryTextTheme + .headline6! + .color!, + ), + ), + ) + : Offstage(), + ], + ), + ), + ], ), ), - Divider( - color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, - height: 1, ), - if (widget.selectedAtIndex != -1) buildSelectedItem(), - Flexible( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - items.length > 3 ? Scrollbar( - controller: controller, - child: itemsList(), - ) : itemsList(), - (widget.description?.isNotEmpty ?? false) - ? Positioned( - bottom: 24, - left: 24, - right: 24, - child: Text( - widget.description!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - fontFamily: 'Lato', - decoration: TextDecoration.none, - color: Theme.of(context).primaryTextTheme.headline6!.color!, - ), - ), - ) - : Offstage(), - ], - ), - ), - ], - ), + ), + ) + ], ), - ), + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), + AlertCloseButton(bottom: closeButtonBottom), + ], ), ), - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), - AlertCloseButton(), + // gives the extra spacing using MediaQuery.viewInsets.bottom + // to simulate a keyboard area + SizedBox( + height: bottom, + ) ], ), ); @@ -185,7 +290,7 @@ class _PickerState<Item> extends State<Picker<Item>> { padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, - itemCount: items.isEmpty ? 0 : items.length, + itemCount: filteredItems.isEmpty ? 0 : filteredItems.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, @@ -199,34 +304,42 @@ class _PickerState<Item> extends State<Picker<Item>> { shrinkWrap: true, separatorBuilder: (context, index) => widget.isSeparated ? Divider( - color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, + color: Theme.of(context) + .accentTextTheme + .headline6! + .backgroundColor!, height: 1, ) : const SizedBox(), - itemCount: items.isEmpty ? 0 : items.length, + itemCount: filteredItems.isEmpty ? 0 : filteredItems.length, itemBuilder: (context, index) => buildItem(index), ), ); } Widget buildItem(int index) { - /// don't show selected item in the list view - if (widget.items[widget.selectedAtIndex] == items[index] && !widget.isGridView) { - return const SizedBox(); - } + final item = filteredItems[index]; + final tag = item is Currency ? item.tag : null; - final item = items[index]; - final image = images.isNotEmpty ? images[index] : null; + final icon = item is Currency && item.iconPath != null + ? Image.asset( + item.iconPath!, + height: 20.0, + width: 20.0, + ) + : null; + + final image = images.isNotEmpty ? filteredImages[index] : icon; return GestureDetector( onTap: () { Navigator.of(context).pop(); - onItemSelected(item); + onItemSelected(item!); }, child: Container( height: 55, color: Theme.of(context).accentTextTheme.headline6!.color!, - padding: EdgeInsets.only(left: 24, right: 24), + padding: EdgeInsets.symmetric(horizontal: 24), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: widget.mainAxisAlignment, @@ -236,15 +349,53 @@ class _PickerState<Item> extends State<Picker<Item>> { Expanded( child: Padding( padding: EdgeInsets.only(left: image != null ? 12 : 0), - child: Text( - widget.displayItem?.call(item) ?? item.toString(), - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.headline6!.color!, - decoration: TextDecoration.none, - ), + child: Row( + children: [ + Flexible( + child: Text( + widget.displayItem?.call(item) ?? item.toString(), + softWrap: true, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline6! + .color!, + decoration: TextDecoration.none, + ), + ), + ), + if (tag != null) + Align( + alignment: Alignment.topCenter, + child: Container( + width: 35.0, + height: 18.0, + child: Center( + child: Text( + tag, + style: TextStyle( + fontSize: 7.0, + fontFamily: 'Lato', + color: Theme.of(context) + .textTheme + .bodyText2! + .color!), + ), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + //border: Border.all(color: ), + color: Theme.of(context) + .textTheme + .bodyText2! + .decorationColor!, + ), + ), + ), + ], ), ), ), @@ -254,37 +405,91 @@ class _PickerState<Item> extends State<Picker<Item>> { ); } - Widget buildSelectedItem() { - final item = widget.items[widget.selectedAtIndex]; - final image = images.isNotEmpty ? widget.images[widget.selectedAtIndex] : null; + Widget buildSelectedItem(int index) { + final item = items[index]; + final tag = item is Currency ? item.tag : null; - return Container( - height: 55, - color: Theme.of(context).accentTextTheme.headline6!.color!, - padding: EdgeInsets.only(left: 24, right: 24), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: widget.mainAxisAlignment, - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - image ?? Offstage(), - Expanded( - child: Padding( - padding: EdgeInsets.only(left: image != null ? 12 : 0), - child: Text( - widget.displayItem?.call(item) ?? item.toString(), - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w700, - color: Theme.of(context).primaryTextTheme.headline6!.color!, - decoration: TextDecoration.none, + final icon = item is Currency && item.iconPath != null + ? Image.asset( + item.iconPath!, + height: 20.0, + width: 20.0, + ) + : null; + + final image = images.isNotEmpty ? images[index] : icon; + + return GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + height: 55, + color: Theme.of(context).accentTextTheme.headline6!.color!, + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: widget.mainAxisAlignment, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + image ?? Offstage(), + Expanded( + child: Padding( + padding: EdgeInsets.only(left: image != null ? 12 : 0), + child: Row( + children: [ + Flexible( + child: Text( + widget.displayItem?.call(item) ?? item.toString(), + softWrap: true, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w700, + color: Theme.of(context) + .primaryTextTheme + .headline6! + .color!, + decoration: TextDecoration.none, + ), + ), + ), + if (tag != null) + Align( + alignment: Alignment.topCenter, + child: Container( + width: 35.0, + height: 18.0, + child: Center( + child: Text( + tag, + style: TextStyle( + fontSize: 7.0, + fontFamily: 'Lato', + color: Theme.of(context) + .textTheme + .bodyText2! + .color!), + ), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + //border: Border.all(color: ), + color: Theme.of(context) + .textTheme + .bodyText2! + .decorationColor!, + ), + ), + ), + ], ), ), ), - ), - Icon(Icons.check_circle, color: Theme.of(context).accentTextTheme.bodyText1!.color!), - ], + Icon(Icons.check_circle, + color: Theme.of(context).accentTextTheme.bodyText1!.color!), + ], + ), ), ); } diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index f874d62a4..4db3684a8 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -5,7 +5,8 @@ import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; class SeedLanguageSelector extends StatefulWidget { - SeedLanguageSelector({Key? key, required this.initialSelected}) : super(key: key); + SeedLanguageSelector({Key? key, required this.initialSelected}) + : super(key: key); final String initialSelected; @@ -30,21 +31,20 @@ class SeedLanguageSelectorState extends State<SeedLanguageSelector> { S.current.seed_language_italian, ]; String selected; - final _pickerKey = GlobalKey<SeedLanguagePickerState>(); @override Widget build(BuildContext context) { return SelectButton( - image: null, - text: seedLocales[seedLanguages.indexOf(selected)], - onTap: () async { - final selected = await showPopUp<String>( - context: context, - builder: (BuildContext context) => - SeedLanguagePicker(key: _pickerKey, selected: this.selected)); - if (selected != null) { - setState(() => this.selected = selected); - } - }); + image: null, + text: seedLocales[seedLanguages.indexOf(selected)], + onTap: () async { + await showPopUp<String>( + context: context, + builder: (_) => SeedLanguagePicker( + selected: this.selected, + onItemSelected: (String selected) => + setState(() => this.selected = selected))); + }, + ); } } From 375755919a0cfaf660b7d7689dd5b9bce8cff4d5 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Fri, 14 Apr 2023 18:09:37 +0200 Subject: [PATCH 08/28] Change Fork source to be from Cake Repo (#881) --- pubspec_base.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec_base.yaml b/pubspec_base.yaml index b357619bd..d427a56d8 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -37,7 +37,7 @@ dependencies: #package_info_plus: ^1.4.2 devicelocale: git: - url: https://github.com/OmarHatem28/flutter-devicelocale + url: https://github.com/cake-tech/flutter-devicelocale auto_size_text: ^3.0.0 dotted_border: ^2.0.0+2 smooth_page_indicator: ^1.0.0+2 From 3b69aa86862f2bf7e83e3cf8229f662a057f5244 Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Fri, 14 Apr 2023 21:55:12 +0300 Subject: [PATCH 09/28] CW-273-Don't-add-node-under-Advanced-Privacy-Settings-if-it-already-exists (#876) * don't add existing node * minor fixes * Add back IsExecutingState to connect [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> --- cw_bitcoin/pubspec.lock | 2 +- cw_core/lib/node.dart | 19 +++++++ cw_core/pubspec.lock | 2 +- cw_monero/pubspec.lock | 2 +- .../advanced_privacy_settings_page.dart | 2 +- .../node_create_or_edit_view_model.dart | 52 +++++++++++++------ 6 files changed, 58 insertions(+), 21 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index bfcd9e5a6..4d864059f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -746,5 +746,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=2.19.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 1322c5b78..7df25d6a1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -73,6 +73,25 @@ class Node extends HiveObject with Keyable { } } + @override + bool operator ==(other) => + other is Node && + (other.uriRaw == uriRaw && + other.login == login && + other.password == password && + other.typeRaw == typeRaw && + other.useSSL == useSSL && + other.trusted == trusted); + + @override + int get hashCode => + uriRaw.hashCode ^ + login.hashCode ^ + password.hashCode ^ + typeRaw.hashCode ^ + useSSL.hashCode ^ + trusted.hashCode; + @override dynamic get keyIndex { _keyIndex ??= key; diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 01e19dda4..70652ec35 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -665,5 +665,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=2.19.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 437184a7d..1e33631d5 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -672,5 +672,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=2.19.0 <4.0.0" flutter: ">=3.0.0" diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 05ff65889..cf0708f21 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -105,7 +105,7 @@ class _AdvancedPrivacySettingsBodyState extends State<AdvancedPrivacySettingsBod return; } - widget.nodeViewModel.save(saveAsCurrent: true); + widget.nodeViewModel.save(); } Navigator.pop(context); diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 5b94aaf6d..aba664fc7 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -4,13 +4,11 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; - -import 'node_list_view_model.dart'; +import 'package:collection/collection.dart'; part 'node_create_or_edit_view_model.g.dart'; -class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase - with _$NodeCreateOrEditViewModel; +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel; abstract class NodeCreateOrEditViewModelBase with Store { NodeCreateOrEditViewModelBase(this._nodeSource, this._walletType, this._settingsStore) @@ -48,11 +46,10 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool trusted; @computed - bool get isReady => - address.isNotEmpty && port.isNotEmpty; + bool get isReady => address.isNotEmpty && port.isNotEmpty; - bool get hasAuthCredentials => _walletType == WalletType.monero || - _walletType == WalletType.haven; + bool get hasAuthCredentials => + _walletType == WalletType.monero || _walletType == WalletType.haven; String get uri { var uri = address; @@ -79,22 +76,22 @@ abstract class NodeCreateOrEditViewModelBase with Store { } @action - void setPort (String val) => port = val; + void setPort(String val) => port = val; @action - void setAddress (String val) => address = val; + void setAddress(String val) => address = val; @action - void setLogin (String val) => login = val; + void setLogin(String val) => login = val; @action - void setPassword (String val) => password = val; + void setPassword(String val) => password = val; @action - void setSSL (bool val) => useSSL = val; + void setSSL(bool val) => useSSL = val; @action - void setTrusted (bool val) => trusted = val; + void setTrusted(bool val) => trusted = val; @action Future<void> save({Node? editingNode, bool saveAsCurrent = false}) async { @@ -109,11 +106,14 @@ abstract class NodeCreateOrEditViewModelBase with Store { state = IsExecutingState(); if (editingNode != null) { await _nodeSource.put(editingNode.key, node); + } else if (existingNode(node) != null) { + setAsCurrent(existingNode(node)!); } else { await _nodeSource.add(node); + setAsCurrent(_nodeSource.values.last); } if (saveAsCurrent) { - _settingsStore.nodes[_walletType] = node; + setAsCurrent(node); } state = ExecutedSuccessfullyState(); @@ -124,14 +124,32 @@ abstract class NodeCreateOrEditViewModelBase with Store { @action Future<void> connect() async { + final node = Node( + uri: uri, + type: _walletType, + login: login, + password: password, + useSSL: useSSL, + trusted: trusted); try { connectionState = IsExecutingState(); - final node = - Node(uri: uri, type: _walletType, login: login, password: password); final isAlive = await node.requestNode(); connectionState = ExecutedSuccessfullyState(payload: isAlive); } catch (e) { connectionState = FailureState(e.toString()); } } + + Node? existingNode(Node node) { + final nodes = _nodeSource.values.toList(); + nodes.forEach((item) { + item.login ??= ''; + item.password ??= ''; + item.useSSL ??= false; + }); + return nodes.firstWhereOrNull((item) => item == node); + } + + @action + void setAsCurrent(Node node) => _settingsStore.nodes[_walletType] = node; } From 77ab6b49f4178ad4d8ddc090dd783a9a3f956c88 Mon Sep 17 00:00:00 2001 From: Rafael Saes <76502841+saltrafael@users.noreply.github.com> Date: Sun, 16 Apr 2023 13:27:33 +0000 Subject: [PATCH 10/28] feat: Add Click to Copy for the Exchange Amount in the invoice (#882) Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> --- lib/view_model/exchange/exchange_trade_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 94c874979..194dc9d45 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -140,7 +140,7 @@ abstract class ExchangeTradeViewModelBase with Store { } items.addAll([ - ExchangeTradeItem(title: S.current.amount, data: '${trade.amount}', isCopied: false), + ExchangeTradeItem(title: S.current.amount, data: '${trade.amount}', isCopied: true), ExchangeTradeItem( title: S.current.send_to_this_address('${trade.from}', tagFrom) + ':', data: trade.inputAddress ?? '', From 4a203a43c8ca6eb4179321cacf8c3c5155b7200f Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Sun, 16 Apr 2023 14:45:35 +0100 Subject: [PATCH 11/28] CW-331-Add-option-to-disable-market-place-in-display-settings (#872) * CW-331-Add-option-to-disable-market-place-in-display-settings [skip ci] * CW-331-Add-option-to-disable-market-place-in-display-settings [skip ci] --- lib/entities/preferences_key.dart | 1 + lib/src/screens/dashboard/dashboard_page.dart | 60 +++++++++++++------ .../settings/display_settings_page.dart | 7 +++ lib/store/settings_store.dart | 16 +++++ .../dashboard/dashboard_view_model.dart | 5 ++ .../settings/display_settings_view_model.dart | 8 +++ res/values/strings_ar.arb | 3 +- res/values/strings_bg.arb | 3 +- res/values/strings_cs.arb | 3 +- res/values/strings_de.arb | 3 +- res/values/strings_en.arb | 3 +- res/values/strings_es.arb | 3 +- res/values/strings_fr.arb | 3 +- res/values/strings_hi.arb | 3 +- res/values/strings_hr.arb | 3 +- res/values/strings_id.arb | 3 +- res/values/strings_it.arb | 3 +- res/values/strings_ja.arb | 3 +- res/values/strings_ko.arb | 3 +- res/values/strings_my.arb | 3 +- res/values/strings_nl.arb | 3 +- res/values/strings_pl.arb | 3 +- res/values/strings_pt.arb | 3 +- res/values/strings_ru.arb | 3 +- res/values/strings_th.arb | 3 +- res/values/strings_tr.arb | 3 +- res/values/strings_uk.arb | 3 +- res/values/strings_ur.arb | 3 +- res/values/strings_zh.arb | 3 +- 29 files changed, 124 insertions(+), 42 deletions(-) diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 8ea30a6f2..589b2a67e 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -39,4 +39,5 @@ class PreferencesKey { static const exchangeProvidersSelection = 'exchange-providers-selection'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; + static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index d0e947508..032ddf9d9 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -109,14 +109,29 @@ class _DashboardPageView extends BasePage { final DashboardViewModel dashboardViewModel; final WalletAddressListViewModel addressListViewModel; - final controller = PageController(initialPage: 1); - - var pages = <Widget>[]; + int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0; + ObservableList<Widget> pages = ObservableList<Widget>(); bool _isEffectsInstalled = false; StreamSubscription<bool>? _onInactiveSub; @override Widget body(BuildContext context) { + final controller = PageController(initialPage: initialPage); + + reaction((_) => dashboardViewModel.shouldShowMarketPlaceInDashboard, (bool value) { + if (!dashboardViewModel.shouldShowMarketPlaceInDashboard) { + controller.jumpToPage(0); + } + pages.clear(); + _isEffectsInstalled = false; + _setEffects(context); + + if (value) { + controller.jumpToPage(1); + } else { + controller.jumpToPage(0); + } + }); _setEffects(context); return SafeArea( @@ -125,23 +140,28 @@ class _DashboardPageView extends BasePage { mainAxisSize: MainAxisSize.max, children: <Widget>[ Expanded( - child: PageView.builder( - controller: controller, - itemCount: pages.length, - itemBuilder: (context, index) => pages[index])), + child: Observer(builder: (context) { + return PageView.builder( + controller: controller, + itemCount: pages.length, + itemBuilder: (context, index) => pages[index]); + })), Padding( padding: EdgeInsets.only(bottom: 24, top: 10), - child: SmoothPageIndicator( - controller: controller, - count: pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).indicatorColor, - activeDotColor: - Theme.of(context).accentTextTheme!.headline4!.backgroundColor!), + child: Observer(builder: (context) { + return SmoothPageIndicator( + controller: controller, + count: pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).indicatorColor, + activeDotColor: + Theme.of(context).accentTextTheme!.headline4!.backgroundColor!), + ); + } )), Observer(builder: (_) { return ClipRect( @@ -201,7 +221,9 @@ class _DashboardPageView extends BasePage { if (_isEffectsInstalled) { return; } - pages.add(MarketPlacePage(dashboardViewModel: dashboardViewModel)); + if (dashboardViewModel.shouldShowMarketPlaceInDashboard) { + pages.add(MarketPlacePage(dashboardViewModel: dashboardViewModel)); + } pages.add(balancePage); pages.add(TransactionsPage(dashboardViewModel: dashboardViewModel)); _isEffectsInstalled = true; diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index 4f932b189..c7baa9b6a 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -34,6 +34,13 @@ class DisplaySettingsPage extends BasePage { onValueChange: (_, bool value) { _displaySettingsViewModel.setShouldDisplayBalance(value); }), + SettingsSwitcherCell( + title: S.current.show_market_place, + value: _displaySettingsViewModel.shouldShowMarketPlaceInDashboard, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShouldShowMarketPlaceInDashbaord(value); + }, + ), //if (!isHaven) it does not work correctly if(!_displaySettingsViewModel.disabledFiatApiMode) SettingsPickerCell<FiatCurrency>( diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 02eb51da7..2080c0a7c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -27,6 +27,7 @@ class SettingsStore = SettingsStoreBase with _$SettingsStore; abstract class SettingsStoreBase with Store { SettingsStoreBase( {required SharedPreferences sharedPreferences, + required bool initialShouldShowMarketPlaceInDashboard, required FiatCurrency initialFiatCurrency, required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, @@ -54,6 +55,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = initialSaveRecipientAddress, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, + shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, pinCodeLength = initialPinLength, @@ -133,6 +135,11 @@ abstract class SettingsStoreBase with Store { PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); + reaction( + (_) => shouldShowMarketPlaceInDashboard, + (bool value) => + sharedPreferences.setBool(PreferencesKey.shouldShowMarketPlaceInDashboard, value)); + reaction( (_) => pinCodeLength, (int pinLength) => sharedPreferences.setInt( @@ -177,6 +184,9 @@ abstract class SettingsStoreBase with Store { @observable bool shouldShowYatPopup; + @observable + bool shouldShowMarketPlaceInDashboard; + @observable ObservableList<ActionListDisplayMode> actionlistDisplayMode; @@ -285,6 +295,8 @@ abstract class SettingsStoreBase with Store { final allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; + final shouldShowMarketPlaceInDashboard = + sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; final exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences .getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); @@ -348,6 +360,7 @@ abstract class SettingsStoreBase with Store { return SettingsStore( sharedPreferences: sharedPreferences, + initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, nodes: nodes, appVersion: packageInfo.version, isBitcoinBuyEnabled: isBitcoinBuyEnabled, @@ -402,6 +415,9 @@ abstract class SettingsStoreBase with Store { allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; + shouldShowMarketPlaceInDashboard = + sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? + shouldShowMarketPlaceInDashboard; exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences .getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index b23f430f9..137dc0a17 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -213,6 +213,11 @@ abstract class DashboardViewModelBase with Store { @computed BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode; + + @computed + bool get shouldShowMarketPlaceInDashboard { + return appStore.settingsStore.shouldShowMarketPlaceInDashboard; + } @computed List<TradeListItem> get trades => tradesStore.trades diff --git a/lib/view_model/settings/display_settings_view_model.dart b/lib/view_model/settings/display_settings_view_model.dart index bac6d9994..69d82eff4 100644 --- a/lib/view_model/settings/display_settings_view_model.dart +++ b/lib/view_model/settings/display_settings_view_model.dart @@ -28,6 +28,9 @@ abstract class DisplaySettingsViewModelBase with Store { @computed bool get shouldDisplayBalance => balanceDisplayMode == BalanceDisplayMode.displayableBalance; + @computed + bool get shouldShowMarketPlaceInDashboard => _settingsStore.shouldShowMarketPlaceInDashboard; + @computed ThemeBase get theme => _settingsStore.currentTheme; @@ -58,4 +61,9 @@ abstract class DisplaySettingsViewModelBase with Store { @action void setFiatCurrency(FiatCurrency value) => _settingsStore.fiatCurrency = value; + + @action + void setShouldShowMarketPlaceInDashbaord(bool value) { + _settingsStore.shouldShowMarketPlaceInDashboard = value; + } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 430f2edcf..1bf0a27b1 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -696,5 +696,6 @@ "clearnet_link": "رابط Clearnet", "onion_link": "رابط البصل", "settings": "إعدادات", - "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن" + "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن", + "show_market_place": "إظهار السوق" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index c879f4d42..0929ad181 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -697,5 +697,6 @@ "optional_name": "Незадължително име на получател", "clearnet_link": "Clearnet връзка", "onion_link": "Лукова връзка", - "sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа" + "sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа", + "show_market_place":"Покажи пазар" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index e8ce7f25c..dbfa086ce 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -697,5 +697,6 @@ "optional_name": "Volitelné jméno příjemce", "clearnet_link": "Odkaz na Clearnet", "onion_link": "Cibulový odkaz", - "sell_monero_com_alert_content": "Prodej Monero zatím není podporován" + "sell_monero_com_alert_content": "Prodej Monero zatím není podporován", + "show_market_place": "Zobrazit trh" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 3896fb133..1d016a716 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -698,5 +698,6 @@ "clearnet_link": "Clearnet-Link", "onion_link": "Zwiebel-Link", "settings": "Einstellungen", - "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt" + "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt", + "show_market_place": "Marktplatz anzeigen" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index fd3cdbc99..316f49f1c 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -698,5 +698,6 @@ "decimal_places_error": "Too many decimal places", "edit_node": "Edit Node", "settings": "Settings", - "sell_monero_com_alert_content": "Selling Monero is not supported yet" + "sell_monero_com_alert_content": "Selling Monero is not supported yet", + "show_market_place" :"Show Marketplace" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 2995008ac..c376b869d 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -698,5 +698,6 @@ "clearnet_link": "enlace Clearnet", "onion_link": "Enlace de cebolla", "settings": "Configuraciones", - "sell_monero_com_alert_content": "Aún no se admite la venta de Monero" + "sell_monero_com_alert_content": "Aún no se admite la venta de Monero", + "show_market_place": "Mostrar mercado" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4dd8e54cb..4dd2b74a9 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -698,5 +698,6 @@ "clearnet_link": "Lien Clearnet", "settings": "Paramètres", "onion_link": "Lien .onion", - "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge" + "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge", + "show_market_place" :"Afficher la place de marché" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index e8280bae7..164b8a4a6 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -698,5 +698,6 @@ "clearnet_link": "क्लियरनेट लिंक", "onion_link": "प्याज का लिंक", "settings": "समायोजन", - "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है" + "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है", + "show_market_place":"बाज़ार दिखाएँ" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 973aaca59..f0d04ed36 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -698,5 +698,6 @@ "clearnet_link": "Clearnet veza", "onion_link": "Poveznica luka", "settings": "Postavke", - "sell_monero_com_alert_content": "Prodaja Monera još nije podržana" + "sell_monero_com_alert_content": "Prodaja Monera još nije podržana", + "show_market_place" : "Prikaži tržište" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 792b828ed..501280351 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -679,5 +679,6 @@ "optional_name": "Nama penerima opsional", "clearnet_link": "Tautan clearnet", "onion_link": "Tautan bawang", - "sell_monero_com_alert_content": "Menjual Monero belum didukung" + "sell_monero_com_alert_content": "Menjual Monero belum didukung", + "show_market_place": "Tampilkan Pasar" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4b90f8890..bbc3a4f3f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -698,5 +698,6 @@ "clearnet_link": "Collegamento Clearnet", "onion_link": "Collegamento a cipolla", "settings": "Impostazioni", - "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata" + "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata", + "show_market_place":"Mostra mercato" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 7caa1b170..2c3b1c841 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -698,5 +698,6 @@ "clearnet_link": "クリアネット リンク", "onion_link": "オニオンリンク", "settings": "設定", - "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません" + "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません", + "show_market_place":"マーケットプレイスを表示" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 327da90db..41d5ba014 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -698,5 +698,6 @@ "clearnet_link": "클리어넷 링크", "onion_link": "양파 링크", "settings": "설정", - "sell_monero_com_alert_content": "지원되지 않습니다." + "sell_monero_com_alert_content": "지원되지 않습니다.", + "show_market_place":"마켓플레이스 표시" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index aaa19d5e7..1ff29dd8f 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -698,5 +698,6 @@ "clearnet_link": "Clearnet လင့်ခ်", "onion_link": "ကြက်သွန်လင့်", "settings": "ဆက်တင်များ", - "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။" + "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။", + "show_market_place":"စျေးကွက်ကိုပြသပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 875c4e5c0..e01bf15d7 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -698,5 +698,6 @@ "clearnet_link": "Clearnet-link", "onion_link": "Ui koppeling", "settings": "Instellingen", - "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund" + "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund", + "show_market_place":"Toon Marktplaats" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index b2b1fcfa5..aba2bf1fb 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -698,5 +698,6 @@ "clearnet_link": "łącze Clearnet", "onion_link": "Łącznik cebulowy", "settings": "Ustawienia", - "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana" + "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana", + "show_market_place" : "Pokaż rynek" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3ac194e7e..a3308f1ce 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -697,5 +697,6 @@ "clearnet_link": "link clear net", "onion_link": "ligação de cebola", "settings": "Configurações", - "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada" + "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada", + "show_market_place":"Mostrar mercado" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d9d5e1d4f..f7202b472 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -698,5 +698,6 @@ "clearnet_link": "Клирнет ссылка", "onion_link": "Луковая ссылка", "settings": "Настройки", - "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается" + "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается", + "show_market_place":"Показать торговую площадку" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 4b400caea..88d12e091 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -696,5 +696,6 @@ "clearnet_link": "ลิงค์เคลียร์เน็ต", "onion_link": "ลิงค์หัวหอม", "settings": "การตั้งค่า", - "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero" + "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero", + "show_market_place":"แสดงตลาดกลาง" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b8fd76499..fcd09dda2 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -698,5 +698,6 @@ "clearnet_link": "Net bağlantı", "onion_link": "soğan bağlantısı", "settings": "ayarlar", - "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor" + "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor", + "show_market_place":"Pazar Yerini Göster" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 1b9995893..4698ac46c 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -697,5 +697,6 @@ "clearnet_link": "Посилання Clearnet", "onion_link": "Посилання на цибулю", "settings": "Налаштування", - "sell_monero_com_alert_content": "Продаж Monero ще не підтримується" + "sell_monero_com_alert_content": "Продаж Monero ще не підтримується", + "show_market_place":"Шоу Ринок" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 7c46baf40..3f3b5efc4 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -698,5 +698,6 @@ "optional_name": "اختیاری وصول کنندہ کا نام", "clearnet_link": "کلیرنیٹ لنک", "onion_link": "پیاز کا لنک", - "sell_monero_com_alert_content": "Monero فروخت کرنا ابھی تک تعاون یافتہ نہیں ہے۔" + "sell_monero_com_alert_content": "Monero فروخت کرنا ابھی تک تعاون یافتہ نہیں ہے۔", + "show_market_place":"بازار دکھائیں۔" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 54c3c14a9..c0fa68d72 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -697,5 +697,6 @@ "clearnet_link": "明网链接", "onion_link": "洋葱链接", "settings": "设置", - "sell_monero_com_alert_content": "尚不支持出售门罗币" + "sell_monero_com_alert_content": "尚不支持出售门罗币", + "show_market_place" :"显示市场" } From a472527c6f19f2a3a96956e4dc0a5ca1c7bd7022 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Mon, 17 Apr 2023 21:06:58 +0300 Subject: [PATCH 12/28] CW-33 Add app review after successful transactions and exchange (#864) * Add app reveiw on transactions and exchange * Update Onramper page * Fix issues in popup review --- lib/entities/preferences_key.dart | 2 ++ .../exchange_trade/exchange_trade_page.dart | 7 +++-- lib/src/screens/send/send_page.dart | 9 ++++-- lib/utils/request_review_handler.dart | 29 +++++++++++++++++++ .../dashboard/dashboard_view_model.dart | 2 +- pubspec_base.yaml | 1 + 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 lib/utils/request_review_handler.dart diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 589b2a67e..be93300a7 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -31,6 +31,8 @@ class PreferencesKey { static const pinTimeOutDuration = 'pin_timeout_duration'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; + static const lastAppReviewDate = 'last_app_review_date'; + static String moneroWalletUpdateV1Key(String name) diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 9eb17c762..f06e879a9 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,4 +1,5 @@ import 'dart:ui'; +import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; @@ -351,8 +352,10 @@ class ExchangeTradeState extends State<ExchangeTradeForm> { right: 24, bottom: 24, child: PrimaryButton( - onPressed: () => - Navigator.of(popupContext).pop(), + onPressed: () { + Navigator.of(popupContext).pop(); + RequestReviewHandler.requestReview(); + }, text: S.of(popupContext).send_got_it, color: Theme.of(popupContext) .accentTextTheme! diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index c30c46565..544bed39c 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:flutter/material.dart'; @@ -383,9 +384,11 @@ class SendPage extends BasePage { alertContent: S.of(context).send_success( sendViewModel.selectedCryptoCurrency.toString()), buttonText: S.of(context).ok, - buttonAction: () => - Navigator.of(context).pop()); - } + buttonAction: () { + Navigator.of(context).pop(); + RequestReviewHandler.requestReview(); + }); + } return Offstage(); }); diff --git a/lib/utils/request_review_handler.dart b/lib/utils/request_review_handler.dart new file mode 100644 index 000000000..487a360bf --- /dev/null +++ b/lib/utils/request_review_handler.dart @@ -0,0 +1,29 @@ +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:in_app_review/in_app_review.dart'; + +class RequestReviewHandler { + static const _coolDownDurationInDays = 30; + + static void requestReview() async { + final sharedPrefs = await SharedPreferences.getInstance(); + + final lastReviewRequestDate = + DateTime.tryParse(sharedPrefs.getString(PreferencesKey.lastAppReviewDate) ?? '') ?? + DateTime.now().subtract(Duration(days: _coolDownDurationInDays + 1)); + + final durationSinceLastRequest = DateTime.now().difference(lastReviewRequestDate).inDays; + + if (durationSinceLastRequest < _coolDownDurationInDays) { + return; + } + + sharedPrefs.setString(PreferencesKey.lastAppReviewDate, DateTime.now().toString()); + + final InAppReview inAppReview = InAppReview.instance; + + if (await inAppReview.isAvailable()) { + inAppReview.requestReview(); + } + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 137dc0a17..6cfbe1455 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -146,7 +146,7 @@ abstract class DashboardViewModelBase with Store { } reaction((_) => appStore.wallet, _onWalletChange); - + connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, transactions, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index d427a56d8..c595ce210 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -67,6 +67,7 @@ dependencies: wakelock: ^0.6.2 flutter_mailer: ^2.0.2 device_info_plus: 8.1.0 + in_app_review: ^2.0.6 cake_backup: git: url: https://github.com/cake-tech/cake_backup.git From 9e7009f3392b4092f73af8431c7a9e789bfe712f Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Mon, 17 Apr 2023 21:09:26 +0200 Subject: [PATCH 13/28] V4.6.2 Bug Fixes (#874) * Added a temporary workaround for empty receive addresses * Fix Typo in PR template * Make existing node private [skip ci] * Fix transactions page background color [skip ci] * Update ios version for internal test build [skip ci] * update macos version for internal test build [skip ci] --- .github/pull_request_template.md | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 16 +++++++++++----- ios/Podfile.lock | 6 ++++++ .../dashboard/widgets/transactions_page.dart | 5 ++++- .../node_create_or_edit_view_model.dart | 6 +++--- scripts/ios/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 4 ++-- 7 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4eb4ffac5..18ad16e4b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,6 +8,6 @@ Please include a summary of the changes and which issue is fixed / feature is ad - [ ] Initial Manual Tests Passed - [ ] Double check modified code and verify it with the feature/task requirements -- [ ] Formate code +- [ ] Format code - [ ] Look for code duplication - [ ] Clear naming for variables and methods diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index eb7a0f61e..741c2fe1c 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -48,7 +48,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override @computed - String get address => receiveAddresses.first.address; + String get address { + if (receiveAddresses.isEmpty) { + return generateNewAddress().address; + } + + return receiveAddresses.first.address; + } @override set address(String addr) => null; @@ -121,8 +127,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - Future<BitcoinAddressRecord> generateNewAddress( - {bitcoin.HDWallet? hd, bool isHidden = false}) async { + BitcoinAddressRecord generateNewAddress( + {bitcoin.HDWallet? hd, bool isHidden = false}) { currentReceiveAddressIndex += 1; // FIX-ME: Check logic for whichi HD should be used here ??? final address = BitcoinAddressRecord( @@ -165,7 +171,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { var hasAddrUse = true; List<BitcoinAddressRecord> addrs; - + if (addresses.isNotEmpty) { addrs = addresses .where((addr) => addr.isHidden == isHidden) @@ -179,7 +185,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { hd: hd, isHidden: isHidden); } - + while(hasAddrUse) { final addr = addrs.last.address; hasAddrUse = await _hasAddressUsed(addr); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 658d55509..3a23117b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -146,6 +146,8 @@ PODS: - CryptoSwift - url_launcher_ios (0.0.1): - Flutter + - wakelock (0.0.1): + - Flutter DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) @@ -173,6 +175,7 @@ DEPENDENCIES: - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - wakelock (from `.symlinks/plugins/wakelock/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -235,6 +238,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + wakelock: + :path: ".symlinks/plugins/wakelock/ios" SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 @@ -271,6 +276,7 @@ SPEC CHECKSUMS: uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f PODFILE CHECKSUM: ae71bdf0eb731a1ffc399c122f6aa4dea0cb5f6f diff --git a/lib/src/screens/dashboard/widgets/transactions_page.dart b/lib/src/screens/dashboard/widgets/transactions_page.dart index de26d8da6..2efb38e89 100644 --- a/lib/src/screens/dashboard/widgets/transactions_page.dart +++ b/lib/src/screens/dashboard/widgets/transactions_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -25,7 +26,9 @@ class TransactionsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - color: Theme.of(context).backgroundColor, + color: ResponsiveLayoutUtil.instance.isMobile(context) + ? null + : Theme.of(context).backgroundColor, padding: EdgeInsets.only(top: 24, bottom: 24), child: Column( children: <Widget>[ diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index aba664fc7..5d84a27df 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -106,8 +106,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { state = IsExecutingState(); if (editingNode != null) { await _nodeSource.put(editingNode.key, node); - } else if (existingNode(node) != null) { - setAsCurrent(existingNode(node)!); + } else if (_existingNode(node) != null) { + setAsCurrent(_existingNode(node)!); } else { await _nodeSource.add(node); setAsCurrent(_nodeSource.values.last); @@ -140,7 +140,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { } } - Node? existingNode(Node node) { + Node? _existingNode(Node node) { final nodes = _nodeSource.values.toList(); nodes.forEach((item) { item.login ??= ''; diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 998e48b68..3b700df46 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=40 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.6.2" -CAKEWALLET_BUILD_NUMBER=145 +CAKEWALLET_VERSION="4.6.3" +CAKEWALLET_BUILD_NUMBER=146 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 3fceeb94d..8cb2d86b9 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -15,8 +15,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.0.1" -CAKEWALLET_BUILD_NUMBER=11 +CAKEWALLET_VERSION="1.0.2" +CAKEWALLET_BUILD_NUMBER=12 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 5ad67b62a5ebc97be70e91727ab9801be61d8a3f Mon Sep 17 00:00:00 2001 From: Rafael Saes <76502841+saltrafael@users.noreply.github.com> Date: Tue, 18 Apr 2023 17:36:56 +0000 Subject: [PATCH 14/28] Cw 304 enhance talkback (#887) * fix(#536): add talkback support to missing main and common elements * fix(#564): add talkback support for slidable node items & addresses page * fix: add missing delete button from add pin widget --- lib/src/screens/base_page.dart | 29 +++--- lib/src/screens/dashboard/dashboard_page.dart | 91 +++++++++++-------- .../screens/new_wallet/new_wallet_page.dart | 53 ++++++----- lib/src/screens/pin_code/pin_code_widget.dart | 61 ++++--------- lib/src/screens/receive/receive_page.dart | 50 ++++------ .../screens/receive/widgets/address_cell.dart | 15 ++- .../settings/connection_sync_page.dart | 66 ++++++++------ lib/src/widgets/introducing_card.dart | 91 ++++++++++--------- 8 files changed, 230 insertions(+), 226 deletions(-) diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 123eef65c..28327ad39 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -58,19 +58,24 @@ abstract class BasePage extends StatelessWidget { bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); - return SizedBox( - height: isMobileView ? 37 : 45, - width: isMobileView ? 37 : 45, - child: ButtonTheme( - minWidth: double.minPositive, - child: TextButton( - style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: canUseCloseIcon && !isMobileView ? 'Close' : 'Back', + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: + canUseCloseIcon && !isMobileView ? _closeButton : _backButton, ), - onPressed: () => onClose(context), - child: canUseCloseIcon && !isMobileView - ? _closeButton - : _backButton, + ), ), ), ); diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 032ddf9d9..fbd976aa8 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -104,7 +104,7 @@ class _DashboardPageView extends BasePage { //splashColor: Colors.transparent, //padding: EdgeInsets.all(0), onPressed: () => onOpenEndDrawer(), - child: menuButton)); + child: Semantics(label: 'Menu', child: menuButton))); } final DashboardViewModel dashboardViewModel; @@ -149,17 +149,21 @@ class _DashboardPageView extends BasePage { Padding( padding: EdgeInsets.only(bottom: 24, top: 10), child: Observer(builder: (context) { - return SmoothPageIndicator( - controller: controller, - count: pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).indicatorColor, - activeDotColor: - Theme.of(context).accentTextTheme!.headline4!.backgroundColor!), + return ExcludeSemantics( + child: SmoothPageIndicator( + controller: controller, + count: pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).indicatorColor, + activeDotColor: Theme.of(context) + .accentTextTheme! + .headline4! + .backgroundColor!), + ), ); } )), @@ -184,27 +188,38 @@ class _DashboardPageView extends BasePage { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: MainActions.all .where((element) => element.canShow?.call(dashboardViewModel) ?? true) - .map((action) => ActionButton( - image: Image.asset(action.image, - height: 24, - width: 24, - color: action.isEnabled?.call(dashboardViewModel) ?? true - ? Theme.of(context) - .accentTextTheme - .headline2! - .backgroundColor! - : Theme.of(context) - .accentTextTheme - .headline3! - .backgroundColor!), - title: action.name(context), - onClick: () async => await action.onTap(context, dashboardViewModel), - textColor: action.isEnabled?.call(dashboardViewModel) ?? true - ? null - : Theme.of(context) - .accentTextTheme - .headline3! - .backgroundColor!, + .map((action) => Semantics( + button: true, + enabled: (action.isEnabled + ?.call(dashboardViewModel) ?? + true), + child: ActionButton( + image: Image.asset(action.image, + height: 24, + width: 24, + color: action.isEnabled?.call( + dashboardViewModel) ?? + true + ? Theme.of(context) + .accentTextTheme + .headline2! + .backgroundColor! + : Theme.of(context) + .accentTextTheme + .headline3! + .backgroundColor!), + title: action.name(context), + onClick: () async => await action.onTap( + context, dashboardViewModel), + textColor: action.isEnabled + ?.call(dashboardViewModel) ?? + true + ? null + : Theme.of(context) + .accentTextTheme + .headline3! + .backgroundColor!, + ), )) .toList(), ), @@ -222,10 +237,14 @@ class _DashboardPageView extends BasePage { return; } if (dashboardViewModel.shouldShowMarketPlaceInDashboard) { - pages.add(MarketPlacePage(dashboardViewModel: dashboardViewModel)); + pages.add(Semantics( + label: 'Marketplace Page', + child: MarketPlacePage(dashboardViewModel: dashboardViewModel))); } - pages.add(balancePage); - pages.add(TransactionsPage(dashboardViewModel: dashboardViewModel)); + pages.add(Semantics(label: 'Balance Page', child: balancePage)); + pages.add(Semantics( + label: 'Transactions Page', + child: TransactionsPage(dashboardViewModel: dashboardViewModel))); _isEffectsInstalled = true; autorun((_) async { diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 0f15e23c5..5fe7522bc 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -139,32 +139,35 @@ class _WalletNameFormState extends State<WalletNameForm> { .decorationColor!, width: 1.0), ), - suffixIcon: IconButton( - onPressed: () async { - final rName = await generateName(); - FocusManager.instance.primaryFocus?.unfocus(); + suffixIcon: Semantics( + label: 'Generate Name', + child: 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! - .headline4! - .decorationColor!, + 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! + .headline4! + .decorationColor!, + ), ), ), ), diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 8f30136d0..0ce4ab333 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -208,58 +208,29 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> { const double marginLeft = 15; if (index == 9) { + // Empty container return Container( margin: EdgeInsets.only(left: marginLeft, right: marginRight), - child: TextButton( - onPressed: () => null, - // (widget.hasLengthSwitcher || - // !settingsStore - // .allowBiometricalAuthentication) - // ? null - // : () { - // FIXME - // if (authStore != null) { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // final biometricAuth = BiometricAuth(); - // biometricAuth.isAuthenticated().then( - // (isAuth) { - // if (isAuth) { - // authStore.biometricAuth(); - // _key.currentState.showSnackBar( - // SnackBar( - // content: Text(S.of(context).authenticated), - // backgroundColor: Colors.green, - // ), - // ); - // } - // } - // ); - // }); - // } - // }, - // FIX-ME: Style - //color: Theme.of(context).backgroundColor, - //shape: CircleBorder(), - child: Container() - // (widget.hasLengthSwitcher || - // !settingsStore - // .allowBiometricalAuthentication) - // ? Offstage() - // : faceImage, - ), ); } else if (index == 10) { index = 0; } else if (index == 11) { - return Container( - margin: EdgeInsets.only(left: marginLeft, right: marginRight), - child: TextButton( - onPressed: () => _pop(), - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).backgroundColor, - shape: CircleBorder(), + return MergeSemantics( + child: Container( + margin: EdgeInsets.only(left: marginLeft, right: marginRight), + child: Semantics( + label: 'Delete', + button: true, + onTap: () => _pop(), + child: TextButton( + onPressed: () => _pop(), + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).backgroundColor, + shape: CircleBorder(), + ), + child: deleteIconImage, + ), ), - child: deleteIconImage, ), ); } else { diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 4a573b2e1..53bda35d8 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -41,26 +41,7 @@ class ReceivePage extends BasePage { final FocusNode _cryptoAmountFocus; @override - Widget leading(BuildContext context) { - final _backButton = Icon(Icons.arrow_back_ios, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, - size: 16,); - - return SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: TextButton( - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), - onPressed: () => onClose(context), - child: _backButton), - ), - ); - } + Color get titleColor => Colors.white; @override Widget middle(BuildContext context) { @@ -93,19 +74,22 @@ class ReceivePage extends BasePage { return Material( color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.address.address, - context: context, - ); - }, - icon: shareImage + child: Semantics( + label: 'Share', + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.address.address, + context: context, + ); + }, + icon: shareImage + ), ) ); } diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 137db108b..e1fe77e72 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -70,11 +70,16 @@ class AddressCell extends StatelessWidget { ), ), )); - return Slidable( - key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), - child: cell, + return Semantics( + label: 'Slidable', + selected: isCurrent, + enabled: !isCurrent, + child: Slidable( + key: Key(address), + startActionPane: _actionPane(context), + endActionPane: _actionPane(context), + child: cell, + ), ); } diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 264cae0d3..edcb57945 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -41,9 +41,13 @@ class ConnectionSyncPage extends BasePage { handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - NodeHeaderListRow( - title: S.of(context).add_new_node, - onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode), + Semantics( + button: true, + child: NodeHeaderListRow( + title: S.of(context).add_new_node, + onTap: (_) async => + await Navigator.of(context).pushNamed(Routes.newNode), + ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SizedBox(height: 100), @@ -60,31 +64,39 @@ class ConnectionSyncPage extends BasePage { itemBuilder: (_, sectionIndex, index) { final node = nodeListViewModel.nodes[index]; final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex; - final nodeListRow = NodeListRow( - title: node.uriRaw, - isSelected: isSelected, - isAlive: node.requestNode(), - onTap: (_) async { - if (isSelected) { - return; - } + final nodeListRow = Semantics( + label: 'Slidable', + selected: isSelected, + enabled: !isSelected, + child: NodeListRow( + title: node.uriRaw, + isSelected: isSelected, + isAlive: node.requestNode(), + onTap: (_) async { + if (isSelected) { + return; + } - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).change_current_node_title, - alertContent: nodeListViewModel.getAlertContent(node.uriRaw), - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).change, - actionLeftButton: () => Navigator.of(context).pop(), - actionRightButton: () async { - await nodeListViewModel.setAsCurrent(node); - Navigator.of(context).pop(); - }, - ); - }); - }, + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: + S.of(context).change_current_node_title, + alertContent: nodeListViewModel + .getAlertContent(node.uriRaw), + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).change, + actionLeftButton: () => + Navigator.of(context).pop(), + actionRightButton: () async { + await nodeListViewModel.setAsCurrent(node); + Navigator.of(context).pop(); + }, + ); + }); + }, + ), ); final dismissibleRow = Slidable( diff --git a/lib/src/widgets/introducing_card.dart b/lib/src/widgets/introducing_card.dart index 52b81fd65..59885d440 100644 --- a/lib/src/widgets/introducing_card.dart +++ b/lib/src/widgets/introducing_card.dart @@ -33,54 +33,59 @@ class IntroducingCard extends StatelessWidget { 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! - .headline2! - .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! - .headline2! - .backgroundColor!, - height: 1)), - ], + child: MergeSemantics( + 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! + .headline2! + .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! + .headline2! + .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, - )), + child: Semantics( + label: 'Close', + 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, + )), + ), ), ), ) From 27961f2f25a77ef16dad2bf528f08118c7044023 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Wed, 19 Apr 2023 16:38:07 +0200 Subject: [PATCH 15/28] Pre-release bug fixes (#888) * Fix Exchange picker UI issue * Fixate local_auth_android version * Remove shared pref android package override since it's fixed --- lib/src/widgets/check_box_picker.dart | 111 ++++++++++-------- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec_base.yaml | 2 +- 4 files changed, 69 insertions(+), 52 deletions(-) diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index a59dda905..e874f587a 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -33,62 +33,71 @@ class CheckBoxPickerState extends State<CheckBoxPicker> { @override Widget build(BuildContext context) { return AlertBackground( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - if (widget.title.isNotEmpty) - 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.headline6!.color!, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.65, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, + child: Column( + children: [ + Expanded( + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + if (widget.title.isNotEmpty) + 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, + ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - items.length > 3 - ? Scrollbar( - controller: controller, - child: itemsList(), - ) - : itemsList(), - ], - ), + ), + 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.headline6!.color!, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.65, + maxWidth: ResponsiveLayoutUtil.kPopupWidth, ), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + items.length > 3 + ? Scrollbar( + controller: controller, + child: itemsList(), + ) + : itemsList(), + ], + ), + ), + ], + ), + ), ), ), ), - ), + ], ), - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), - AlertCloseButton(), - ], - ), + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), + AlertCloseButton(), + ], + ), + ), + ], ), ); } @@ -146,7 +155,7 @@ class CheckBoxPickerState extends State<CheckBoxPicker> { if (value == null) { return; } - + item.value = value; widget.onChanged(index, value); setState(() {}); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index feebda3f2..437237153 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import cw_monero import device_info_plus import devicelocale import flutter_secure_storage_macos +import in_app_review import package_info import path_provider_foundation import platform_device_id @@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) + InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 41861493e..a56166e28 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -26,6 +26,8 @@ PODS: - flutter_secure_storage_macos (3.3.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - in_app_review (0.2.0): + - FlutterMacOS - package_info (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -53,6 +55,7 @@ DEPENDENCIES: - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) - platform_device_id (from `Flutter/ephemeral/.symlinks/plugins/platform_device_id/macos`) @@ -79,6 +82,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral + in_app_review: + :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos package_info: :path: Flutter/ephemeral/.symlinks/plugins/package_info/macos path_provider_foundation: @@ -103,6 +108,7 @@ SPEC CHECKSUMS: devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 platform_device_id: 3e414428f45df149bbbfb623e2c0ca27c545b763 diff --git a/pubspec_base.yaml b/pubspec_base.yaml index c595ce210..8beb79116 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -9,7 +9,6 @@ dependencies: qr_flutter: ^4.0.0 uuid: 3.0.6 shared_preferences: ^2.0.15 - shared_preferences_android: 2.1.0 flutter_secure_storage: git: url: https://github.com/cake-tech/flutter_secure_storage.git @@ -33,6 +32,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 local_auth: ^2.1.0 + local_auth_android: 1.0.21 package_info: ^2.0.0 #package_info_plus: ^1.4.2 devicelocale: From efef30f8eba78ebd8ab5933098701bb2e3138dc1 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Thu, 20 Apr 2023 03:54:25 +0300 Subject: [PATCH 16/28] CW-278 Enhance PIN timeout feature code (#886) * CW-278 enhance pin timeout feature * CW-278 enhance pin timeout feature * Update flow to remove extension * Replace pin request on other instances --- lib/core/auth_service.dart | 39 +++++++- lib/di.dart | 6 +- .../desktop_wallet_selection_dropdown.dart | 32 +++---- .../settings/security_backup_page.dart | 45 ++++----- .../screens/wallet_list/wallet_list_page.dart | 91 +++++++------------ 5 files changed, 102 insertions(+), 111 deletions(-) diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 54f89437a..d26fd17a3 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -1,10 +1,12 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/encrypt.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; class AuthService with Store { @@ -14,6 +16,12 @@ class AuthService with Store { required this.settingsStore, }); + static const List<String> _alwaysAuthenticateRoutes = [ + Routes.showKeys, + Routes.backup, + Routes.setupPin, + ]; + final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; final SettingsStore settingsStore; @@ -66,4 +74,33 @@ class AuthService with Store { return timeDifference.inMinutes; } + + Future<void> authenticateAction(BuildContext context, + {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async { + assert(route != null || onAuthSuccess != null, + 'Either route or onAuthSuccess param must be passed.'); + if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (onAuthSuccess != null) { + onAuthSuccess(true); + } else { + Navigator.of(context).pushNamed( + route ?? '', + arguments: arguments, + ); + } + return; + } + Navigator.of(context).pushNamed(Routes.auth, + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + if (!isAuthenticatedSuccessfully) { + onAuthSuccess?.call(false); + return; + } + if (onAuthSuccess != null) { + auth.close().then((value) => onAuthSuccess.call(true)); + } else { + auth.close(route: route, arguments: arguments); + } + }); + } } diff --git a/lib/di.dart b/lib/di.dart index 2d6e1ec34..584bc8b8e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -499,7 +499,7 @@ Future setup( } getIt.registerFactory(() => - WalletListPage(walletListViewModel: getIt.get<WalletListViewModel>())); + WalletListPage(walletListViewModel: getIt.get<WalletListViewModel>(), authService: getIt.get<AuthService>(),)); getIt.registerFactory(() { final wallet = getIt.get<AppStore>().wallet!; @@ -593,7 +593,7 @@ Future setup( getIt.registerFactory(() => ConnectionSyncPage(getIt.get<NodeListViewModel>(), getIt.get<DashboardViewModel>())); - getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>())); + getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>())); getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>())); @@ -926,7 +926,7 @@ Future setup( wallet: getIt.get<AppStore>().wallet!) ); - getIt.registerFactory(() => DesktopWalletSelectionDropDown(getIt.get<WalletListViewModel>())); + getIt.registerFactory(() => DesktopWalletSelectionDropDown(getIt.get<WalletListViewModel>(), getIt.get<AuthService>())); getIt.registerFactory(() => DesktopSidebarViewModel()); diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 1ad831b1b..0bfcb359e 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -1,8 +1,9 @@ import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -16,8 +17,10 @@ import 'package:flutter_mobx/flutter_mobx.dart'; class DesktopWalletSelectionDropDown extends StatefulWidget { final WalletListViewModel walletListViewModel; + final AuthService _authService; - DesktopWalletSelectionDropDown(this.walletListViewModel, {Key? key}) : super(key: key); + DesktopWalletSelectionDropDown(this.walletListViewModel, this._authService, {Key? key}) + : super(key: key); @override State<DesktopWalletSelectionDropDown> createState() => _DesktopWalletSelectionDropDownState(); @@ -140,25 +143,12 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD } Future<void> _loadWallet(WalletListItem wallet) async { - if (await widget.walletListViewModel.checkIfAuthRequired()) { - await Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { - if (!isAuthenticatedSuccessfully) { - return; - } + widget._authService.authenticateAction(context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; + } - try { - auth.changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); - await widget.walletListViewModel.loadWallet(wallet); - auth.hideProgressText(); - auth.close(); - setState(() {}); - } catch (e) { - auth.changeProcessText( - S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); - } - }); - } else { try { changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); await widget.walletListViewModel.loadWallet(wallet); @@ -167,7 +157,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD } catch (e) { changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); } - } + }); } void _navigateToCreateWallet() { diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 0cc8aa15d..0933560f7 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -1,6 +1,6 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; @@ -13,7 +13,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class SecurityBackupPage extends BasePage { - SecurityBackupPage(this._securitySettingsViewModel); + SecurityBackupPage(this._securitySettingsViewModel, this._authService); + + final AuthService _authService; @override String get title => S.current.security_and_backup; @@ -27,35 +29,24 @@ class SecurityBackupPage extends BasePage { child: Column(mainAxisSize: MainAxisSize.min, children: [ SettingsCellWithArrow( title: S.current.show_keys, - handler: (_) => Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { - if (isAuthenticatedSuccessfully) { - auth.close(route: Routes.showKeys); - } - }), + handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( title: S.current.create_backup, - handler: (_) => Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { - if (isAuthenticatedSuccessfully) { - auth.close(route: Routes.backup); - } - }), + handler: (_) => _authService.authenticateAction(context, route: Routes.backup), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( - title: S.current.settings_change_pin, - handler: (_) => Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { - auth.close( - route: isAuthenticatedSuccessfully ? Routes.setupPin : null, - arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) { - setupPinContext.close(); - }, - ); - })), + title: S.current.settings_change_pin, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.setupPin, + arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) { + setupPinContext.close(); + }, + ), + ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), Observer(builder: (_) { return SettingsSwitcherCell( @@ -63,8 +54,8 @@ class SecurityBackupPage extends BasePage { value: _securitySettingsViewModel.allowBiometricalAuthentication, onValueChange: (BuildContext context, bool value) { if (value) { - Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + _authService.authenticateAction(context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { if (isAuthenticatedSuccessfully) { if (await _securitySettingsViewModel.biometricAuthenticated()) { _securitySettingsViewModel @@ -74,8 +65,6 @@ class SecurityBackupPage extends BasePage { _securitySettingsViewModel .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); } - - auth.close(); }); } else { _securitySettingsViewModel.setAllowBiometricalAuthentication(value); diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index a1e8c51b7..316203ddd 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -19,18 +19,21 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; class WalletListPage extends BasePage { - WalletListPage({required this.walletListViewModel}); + WalletListPage({required this.walletListViewModel, required this.authService}); final WalletListViewModel walletListViewModel; + final AuthService authService; @override - Widget body(BuildContext context) => WalletListBody(walletListViewModel: walletListViewModel); + Widget body(BuildContext context) => + WalletListBody(walletListViewModel: walletListViewModel, authService: authService); } class WalletListBody extends StatefulWidget { - WalletListBody({required this.walletListViewModel}); + WalletListBody({required this.walletListViewModel, required this.authService}); final WalletListViewModel walletListViewModel; + final AuthService authService; @override WalletListBodyState createState() => WalletListBodyState(); @@ -129,7 +132,8 @@ class WalletListBodyState extends State<WalletListBody> { fontSize: 22, fontWeight: FontWeight.w500, color: Theme.of(context) - .primaryTextTheme.headline6! + .primaryTextTheme + .headline6! .color!), ) ], @@ -201,61 +205,40 @@ class WalletListBodyState extends State<WalletListBody> { } Future<void> _loadWallet(WalletListItem wallet) async { - if (await widget.walletListViewModel.checkIfAuthRequired()) { - await Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { - if (!isAuthenticatedSuccessfully) { - return; - } + await widget.authService.authenticateAction(context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; + } - try { - auth.changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); - await widget.walletListViewModel.loadWallet(wallet); - auth.hideProgressText(); - auth.close(); - // only pop the wallets route in mobile as it will go back to dashboard page - // in desktop platforms the navigation tree is different - if (DeviceInfo.instance.isMobile) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pop(); - }); - } - } catch (e) { - auth.changeProcessText( - S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); - } - }); - } else { try { changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); await widget.walletListViewModel.loadWallet(wallet); - hideProgressText(); + await hideProgressText(); // only pop the wallets route in mobile as it will go back to dashboard page // in desktop platforms the navigation tree is different if (DeviceInfo.instance.isMobile) { - Navigator.of(context).pop(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); + }); } } catch (e) { changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); } - } + }); } Future<void> _removeWallet(WalletListItem wallet) async { - if (widget.walletListViewModel.checkIfAuthRequired()) { - await Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { - if (!isAuthenticatedSuccessfully) { - return; - } - _onSuccessfulAuth(wallet, auth); - }); - } else { - _onSuccessfulAuth(wallet, null); - } + widget.authService.authenticateAction(context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; + } + _onSuccessfulAuth(wallet); + }); } - void _onSuccessfulAuth(WalletListItem wallet, AuthPageState? auth) async { + void _onSuccessfulAuth(WalletListItem wallet) async { bool confirmed = false; await showPopUp<void>( context: context, @@ -275,31 +258,23 @@ class WalletListBodyState extends State<WalletListBody> { if (confirmed) { try { - auth != null - ? auth.changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name)) - : changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name)); + changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name)); await widget.walletListViewModel.remove(wallet); hideProgressText(); } catch (e) { - auth != null - ? auth.changeProcessText( - S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()), - ) - : changeProcessText( - S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()), - ); + changeProcessText( + S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()), + ); } } - - auth?.close(); } void changeProcessText(String text) { _progressBar = createBar<void>(text, duration: null)..show(context); } - void hideProgressText() { - Future.delayed(Duration(milliseconds: 50), () { + Future<void> hideProgressText() async { + await Future.delayed(Duration(milliseconds: 50), () { _progressBar?.dismiss(); _progressBar = null; }); From 7b91b0e938c6f5c3707ecf86b33dd3ecd3b1e4d5 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 20 Apr 2023 02:13:37 +0100 Subject: [PATCH 17/28] Cw 262 better handle user exchange amount below minimum or maximum trade size (#868) * CW-262-Better-handle-user-exchange-amount-below-minimum-or-maximum-trade-size * fix: App should compute conversion even if it's not within the limits --- lib/core/amount_validator.dart | 100 +++++++++++++++++- lib/src/screens/exchange/exchange_page.dart | 30 ++++-- .../exchange/exchange_view_model.dart | 21 +++- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + 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_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.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_th.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_zh.arb | 2 + 26 files changed, 185 insertions(+), 12 deletions(-) diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index 42eb3c32d..acd0ab135 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -3,17 +3,45 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; class AmountValidator extends TextValidator { - AmountValidator({required CryptoCurrency currency, bool isAutovalidate = false}) { + AmountValidator({ + required CryptoCurrency currency, + bool isAutovalidate = false, + String? minValue, + String? maxValue, + }) { symbolsAmountValidator = SymbolsAmountValidator(isAutovalidate: isAutovalidate); decimalAmountValidator = DecimalAmountValidator(currency: currency,isAutovalidate: isAutovalidate); + + amountMinValidator = AmountMinValidator( + minValue: minValue, + isAutovalidate: isAutovalidate, + ); + + amountMaxValidator = AmountMaxValidator( + maxValue: maxValue, + isAutovalidate: isAutovalidate, + ); } + late final AmountMinValidator amountMinValidator; + + late final AmountMaxValidator amountMaxValidator; + late final SymbolsAmountValidator symbolsAmountValidator; late final DecimalAmountValidator decimalAmountValidator; - String? call(String? value) => symbolsAmountValidator(value) ?? decimalAmountValidator(value); + String? call(String? value) { + //* Validate for Text(length, symbols, decimals etc) + + final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value); + + //* Validate for Comparison(Value greater than min and less than ) + final comparisonValidation = amountMinValidator(value) ?? amountMaxValidator(value); + + return textValidation ?? comparisonValidation; + } } class SymbolsAmountValidator extends TextValidator { @@ -57,3 +85,71 @@ class AllAmountValidator extends TextValidator { minLength: 0, maxLength: 0); } + +class AmountMinValidator extends Validator<String> { + final String? minValue; + final bool isAutovalidate; + + AmountMinValidator({ + this.minValue, + required this.isAutovalidate, + }) : super(errorMessage: S.current.error_text_input_below_minimum_limit); + + @override + bool isValid(String? value) { + if (value == null || value.isEmpty) { + return isAutovalidate ? true : false; + } + + if (minValue == null || minValue == "null") { + return true; + } + + final valueInDouble = parseToDouble(value); + final minInDouble = parseToDouble(minValue ?? ''); + + if (valueInDouble == null || minInDouble == null) { + return false; + } + + return valueInDouble > minInDouble; + } + + double? parseToDouble(String value) { + final data = double.tryParse(value.replaceAll(',', '.')); + return data; + } +} + +class AmountMaxValidator extends Validator<String> { + final String? maxValue; + final bool isAutovalidate; + + AmountMaxValidator({ + this.maxValue, + required this.isAutovalidate, + }) : super(errorMessage: S.current.error_text_input_above_maximum_limit); + + @override + bool isValid(String? value) { + if (value == null || value.isEmpty) { + return isAutovalidate ? true : false; + } + + if (maxValue == null || maxValue == "null") { + return true; + } + + final valueInDouble = parseToDouble(value); + final maxInDouble = parseToDouble(maxValue ?? ''); + + if (valueInDouble == null || maxInDouble == null) { + return false; + } + return valueInDouble < maxInDouble; + } + + double? parseToDouble(String value) { + return double.tryParse(value.replaceAll(',', '.')); + } +} diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 99eeae7dc..310e40cd8 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -456,8 +456,7 @@ class ExchangePage extends BasePage { depositAmountController.addListener(() { if (depositAmountController.text != exchangeViewModel.depositAmount) { _depositAmountDebounce.run(() { - exchangeViewModel.changeDepositAmount( - amount: depositAmountController.text); + exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); exchangeViewModel.isReceiveAmountEntered = false; }); } @@ -469,8 +468,7 @@ class ExchangePage extends BasePage { receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { _receiveAmountDebounce.run(() { - exchangeViewModel.changeReceiveAmount( - amount: receiveAmountController.text); + exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); exchangeViewModel.isReceiveAmountEntered = true; }); } @@ -626,8 +624,16 @@ class ExchangePage extends BasePage { currencyButtonColor: Colors.transparent, addressButtonsColor: Theme.of(context).focusColor!, borderColor: Theme.of(context).primaryTextTheme!.bodyText1!.color!, - currencyValueValidator: - AmountValidator(currency: exchangeViewModel.depositCurrency), + currencyValueValidator: (value) { + return !exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.depositCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency), onPushPasteButton: (context) async { @@ -668,8 +674,16 @@ class ExchangePage extends BasePage { addressButtonsColor: Theme.of(context).focusColor!, borderColor: Theme.of(context).primaryTextTheme!.bodyText1!.decorationColor!, - currencyValueValidator: - AmountValidator(currency: exchangeViewModel.receiveCurrency), + currencyValueValidator: (value) { + return exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.receiveCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency), onPushPasteButton: (context) async { diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index bf0d88cd0..1e25f1353 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -198,6 +198,9 @@ abstract class ExchangeViewModelBase with Store { @observable bool isFixedRateMode; + @observable + Limits limits; + @computed SyncStatus get status => wallet.syncStatus; @@ -241,8 +244,6 @@ abstract class ExchangeViewModelBase with Store { List<CryptoCurrency> depositCurrencies; - Limits limits; - NumberFormat _cryptoNumberFormat; final SettingsStore _settingsStore; @@ -320,6 +321,22 @@ abstract class ExchangeViewModelBase with Store { .replaceAll(RegExp('\\,'), ''); } + bool checkIfInputMeetsMinOrMaxCondition(String input) { + final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0; + double minLimit = limits.min ?? 0; + double? maxLimit = limits.max; + + if (_enteredAmount < minLimit) { + return false; + } + + if (maxLimit != null && _enteredAmount > maxLimit) { + return false; + } + + return true; + } + Future<void> _calculateBestRate() async { final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 1bf0a27b1..8dbf1b900 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -697,5 +697,7 @@ "onion_link": "رابط البصل", "settings": "إعدادات", "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن", + "error_text_input_below_minimum_limit":" المبلغ أقل من الحد الأدنى", + "error_text_input_above_maximum_limit":"المبلغ أكبر من الحد الأقصى", "show_market_place": "إظهار السوق" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 0929ad181..2a95efeee 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -698,5 +698,7 @@ "clearnet_link": "Clearnet връзка", "onion_link": "Лукова връзка", "sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа", + "error_text_input_below_minimum_limit" : "Сумата е по-малко от минималната", + "error_text_input_above_maximum_limit" : "Сумата надвишава максималната", "show_market_place":"Покажи пазар" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index dbfa086ce..ddd22eb61 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -698,5 +698,7 @@ "clearnet_link": "Odkaz na Clearnet", "onion_link": "Cibulový odkaz", "sell_monero_com_alert_content": "Prodej Monero zatím není podporován", + "error_text_input_below_minimum_limit" : "Částka je menší než minimální hodnota", + "error_text_input_above_maximum_limit" : "Částka je větší než maximální hodnota", "show_market_place": "Zobrazit trh" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 1d016a716..d29a641e2 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -699,5 +699,7 @@ "onion_link": "Zwiebel-Link", "settings": "Einstellungen", "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt", + "error_text_input_below_minimum_limit" : "Menge ist unter dem Minimum", + "error_text_input_above_maximum_limit" : "Menge ist über dem Maximum", "show_market_place": "Marktplatz anzeigen" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 316f49f1c..f4d693e8f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -699,5 +699,7 @@ "edit_node": "Edit Node", "settings": "Settings", "sell_monero_com_alert_content": "Selling Monero is not supported yet", + "error_text_input_below_minimum_limit" : "Amount is less than the minimum", + "error_text_input_above_maximum_limit" : "Amount is more than the maximum", "show_market_place" :"Show Marketplace" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c376b869d..cee502fd3 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -699,5 +699,7 @@ "onion_link": "Enlace de cebolla", "settings": "Configuraciones", "sell_monero_com_alert_content": "Aún no se admite la venta de Monero", + "error_text_input_below_minimum_limit" : "La cantidad es menos que mínima", + "error_text_input_above_maximum_limit" : "La cantidad es más que el máximo", "show_market_place": "Mostrar mercado" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4dd2b74a9..c4a44895a 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -699,5 +699,7 @@ "settings": "Paramètres", "onion_link": "Lien .onion", "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge", + "error_text_input_below_minimum_limit" : "Le montant est inférieur au minimum", + "error_text_input_above_maximum_limit" : "Le montant est supérieur au maximum", "show_market_place" :"Afficher la place de marché" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 164b8a4a6..2669af5dc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -699,5 +699,7 @@ "onion_link": "प्याज का लिंक", "settings": "समायोजन", "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है", + "error_text_input_below_minimum_limit" : "राशि न्यूनतम से कम है", + "error_text_input_above_maximum_limit" : "राशि अधिकतम से अधिक है", "show_market_place":"बाज़ार दिखाएँ" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index f0d04ed36..865494c8d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -699,5 +699,7 @@ "onion_link": "Poveznica luka", "settings": "Postavke", "sell_monero_com_alert_content": "Prodaja Monera još nije podržana", + "error_text_input_below_minimum_limit" : "Iznos je manji od minimalnog", + "error_text_input_above_maximum_limit" : "Iznos je veći od maskimalnog", "show_market_place" : "Prikaži tržište" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 501280351..12e1f2cfa 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -680,5 +680,7 @@ "clearnet_link": "Tautan clearnet", "onion_link": "Tautan bawang", "sell_monero_com_alert_content": "Menjual Monero belum didukung", + "error_text_input_below_minimum_limit" : "Jumlah kurang dari minimal", + "error_text_input_above_maximum_limit" : "Jumlah lebih dari maksimal", "show_market_place": "Tampilkan Pasar" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index bbc3a4f3f..dce7df8e3 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -699,5 +699,7 @@ "onion_link": "Collegamento a cipolla", "settings": "Impostazioni", "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata", + "error_text_input_below_minimum_limit" : "L'ammontare è inferiore al minimo", + "error_text_input_above_maximum_limit" : "L'ammontare è superiore al massimo", "show_market_place":"Mostra mercato" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 2c3b1c841..674457116 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -699,5 +699,7 @@ "onion_link": "オニオンリンク", "settings": "設定", "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません", + "error_text_input_below_minimum_limit" : "金額は最小額より少ない", + "error_text_input_above_maximum_limit" : "金額は最大値を超えています", "show_market_place":"マーケットプレイスを表示" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 41d5ba014..4b35b313b 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -699,5 +699,7 @@ "onion_link": "양파 링크", "settings": "설정", "sell_monero_com_alert_content": "지원되지 않습니다.", + "error_text_input_below_minimum_limit" : "금액이 최소보다 적습니다.", + "error_text_input_above_maximum_limit" : "금액이 최대 값보다 많습니다.", "show_market_place":"마켓플레이스 표시" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1ff29dd8f..f08634f48 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -699,5 +699,7 @@ "onion_link": "ကြက်သွန်လင့်", "settings": "ဆက်တင်များ", "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။", + "error_text_input_below_minimum_limit" : "ပမာဏသည် အနိမ့်ဆုံးထက်နည်းသည်။", + "error_text_input_above_maximum_limit" : "ပမာဏသည် အများဆုံးထက် ပိုများသည်။", "show_market_place":"စျေးကွက်ကိုပြသပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e01bf15d7..6e416b004 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -699,5 +699,7 @@ "onion_link": "Ui koppeling", "settings": "Instellingen", "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund", + "error_text_input_below_minimum_limit" : "Bedrag is minder dan minimaal", + "error_text_input_above_maximum_limit" : "Bedrag is meer dan maximaal", "show_market_place":"Toon Marktplaats" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index aba2bf1fb..357edc2e3 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -699,5 +699,7 @@ "onion_link": "Łącznik cebulowy", "settings": "Ustawienia", "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana", + "error_text_input_below_minimum_limit" : "Kwota jest mniejsza niż minimalna", + "error_text_input_above_maximum_limit" : "Kwota jest większa niż maksymalna", "show_market_place" : "Pokaż rynek" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index a3308f1ce..d02147050 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -698,5 +698,7 @@ "onion_link": "ligação de cebola", "settings": "Configurações", "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada", + "error_text_input_below_minimum_limit" : "O valor é menor que o mínimo", + "error_text_input_above_maximum_limit" : "O valor é superior ao máximo", "show_market_place":"Mostrar mercado" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index f7202b472..ad14f4cb0 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -699,5 +699,7 @@ "onion_link": "Луковая ссылка", "settings": "Настройки", "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается", + "error_text_input_below_minimum_limit" : "Сумма меньше минимальной", + "error_text_input_above_maximum_limit" : "Сумма больше максимальной", "show_market_place":"Показать торговую площадку" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 88d12e091..b8f229a09 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -697,5 +697,7 @@ "onion_link": "ลิงค์หัวหอม", "settings": "การตั้งค่า", "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero", + "error_text_input_below_minimum_limit" : "จำนวนเงินน้อยกว่าขั้นต่ำ", + "error_text_input_above_maximum_limit" : "จำนวนเงินสูงกว่าค่าสูงสุด", "show_market_place":"แสดงตลาดกลาง" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index fcd09dda2..2ef0b9302 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -699,5 +699,7 @@ "onion_link": "soğan bağlantısı", "settings": "ayarlar", "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor", + "error_text_input_below_minimum_limit" : "Miktar minimumdan daha azdır", + "error_text_input_above_maximum_limit" : "Miktar maksimumdan daha fazla", "show_market_place":"Pazar Yerini Göster" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 4698ac46c..72cccf43b 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -698,5 +698,7 @@ "onion_link": "Посилання на цибулю", "settings": "Налаштування", "sell_monero_com_alert_content": "Продаж Monero ще не підтримується", + "error_text_input_below_minimum_limit" : "Сума менша мінімальної", + "error_text_input_above_maximum_limit" : "Сума більше максимальної", "show_market_place":"Шоу Ринок" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 3f3b5efc4..000ce76b4 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -699,5 +699,7 @@ "clearnet_link": "کلیرنیٹ لنک", "onion_link": "پیاز کا لنک", "sell_monero_com_alert_content": "Monero فروخت کرنا ابھی تک تعاون یافتہ نہیں ہے۔", + "error_text_input_below_minimum_limit" : "رقم کم از کم سے کم ہے۔", + "error_text_input_above_maximum_limit" : "رقم زیادہ سے زیادہ سے زیادہ ہے۔", "show_market_place":"بازار دکھائیں۔" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c0fa68d72..e95e854d9 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -698,5 +698,7 @@ "onion_link": "洋葱链接", "settings": "设置", "sell_monero_com_alert_content": "尚不支持出售门罗币", + "error_text_input_below_minimum_limit" : "金额小于最小值", + "error_text_input_above_maximum_limit" : "金额大于最大值", "show_market_place" :"显示市场" } From f26815efb80a735b12bbf52b4d1e515cb8cb5c13 Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Thu, 20 Apr 2023 12:59:59 +0300 Subject: [PATCH 18/28] CW-351-Add-option-in-Privacy-settings-to-enable-disable-screenshots (#885) * add prevent screenshots option * fix prevent screen recording * update localization * Update strings_ja.arb --- .../cakewallet/cake_wallet/MainActivity.java | 15 ++++++----- cw_core/lib/set_app_secure_native.dart | 6 +++++ cw_monero/example/pubspec.lock | 14 +++++------ lib/core/backup_service.dart | 6 +++++ lib/entities/preferences_key.dart | 1 + lib/src/screens/settings/privacy_page.dart | 8 ++++++ lib/store/settings_store.dart | 25 ++++++++++++++++++- .../settings/privacy_settings_view_model.dart | 6 +++++ res/values/strings_ar.arb | 3 ++- res/values/strings_bg.arb | 3 ++- res/values/strings_cs.arb | 3 ++- res/values/strings_de.arb | 3 ++- res/values/strings_en.arb | 3 ++- res/values/strings_es.arb | 3 ++- res/values/strings_fr.arb | 3 ++- res/values/strings_hi.arb | 3 ++- res/values/strings_hr.arb | 3 ++- res/values/strings_id.arb | 3 ++- res/values/strings_it.arb | 3 ++- res/values/strings_ja.arb | 3 ++- res/values/strings_ko.arb | 3 ++- res/values/strings_my.arb | 3 ++- res/values/strings_nl.arb | 3 ++- res/values/strings_pl.arb | 3 ++- res/values/strings_pt.arb | 3 ++- res/values/strings_ru.arb | 3 ++- res/values/strings_th.arb | 3 ++- res/values/strings_tr.arb | 3 ++- res/values/strings_uk.arb | 3 ++- res/values/strings_ur.arb | 3 ++- res/values/strings_zh.arb | 3 ++- 31 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 cw_core/lib/set_app_secure_native.dart diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 1c1e42df8..edaaefeb2 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -24,6 +24,7 @@ import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; + boolean isAppSecure = false; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { @@ -56,6 +57,14 @@ public class MainActivity extends FlutterFragmentActivity { handler.post(() -> result.success("")); } break; + case "setIsAppSecure": + isAppSecure = call.argument("isAppSecure"); + if (isAppSecure) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + } + break; default: handler.post(() -> result.notImplemented()); } @@ -80,10 +89,4 @@ public class MainActivity extends FlutterFragmentActivity { } }); } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); - } } \ No newline at end of file diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart new file mode 100644 index 000000000..2f2e9a9c9 --- /dev/null +++ b/cw_core/lib/set_app_secure_native.dart @@ -0,0 +1,6 @@ +import 'package:flutter/services.dart'; + +const utils = const MethodChannel('com.cake_wallet/native_utils'); + +void setIsAppSecureNative(bool isAppSecure) => + utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure}); diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index 772ff47bd..19d9cef8f 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -115,10 +115,10 @@ packages: dependency: transitive description: name: ffi - sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905 + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.1.5" platform: dependency: transitive description: @@ -386,10 +386,10 @@ packages: dependency: transitive description: name: win32 - sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.1.4" xdg_directories: dependency: transitive description: @@ -399,5 +399,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=2.18.1 <3.0.0" + dart: ">=2.18.1 <4.0.0" flutter: ">=3.0.0" diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 3cb434efe..20fd753d8 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -209,6 +209,7 @@ class BackupService { final currentBalanceDisplayMode = data[PreferencesKey.currentBalanceDisplayModeKey] as int?; final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?; final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?; + final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; @@ -245,6 +246,11 @@ class BackupService { PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); + if (isAppSecure != null) + await _sharedPreferences.setBool( + PreferencesKey.isAppSecureKey, + isAppSecure); + if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index be93300a7..f5741a98b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -9,6 +9,7 @@ class PreferencesKey { static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; + static const isAppSecureKey = 'is_app_secure'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index b11b41199..81e2715f2 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'dart:io' show Platform; class PrivacyPage extends BasePage { PrivacyPage(this._privacySettingsViewModel); @@ -48,6 +49,13 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setShouldSaveRecipientAddress(value); }), + if (Platform.isAndroid) + SettingsSwitcherCell( + title: S.current.prevent_screenshots, + value: _privacySettingsViewModel.isAppSecure, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setIsAppSecure(value); + }), ], ); }), diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 2080c0a7c..400f7ac88 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -19,6 +19,8 @@ import 'package:cw_core/node.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cw_core/set_app_secure_native.dart'; +import 'dart:io' show Platform; part 'settings_store.g.dart'; @@ -31,6 +33,7 @@ abstract class SettingsStoreBase with Store { required FiatCurrency initialFiatCurrency, required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, + required bool initialAppSecure, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, required ExchangeApiMode initialExchangeStatus, @@ -53,6 +56,7 @@ abstract class SettingsStoreBase with Store { fiatCurrency = initialFiatCurrency, balanceDisplayMode = initialBalanceDisplayMode, shouldSaveRecipientAddress = initialSaveRecipientAddress, + isAppSecure = initialAppSecure, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, @@ -119,6 +123,17 @@ abstract class SettingsStoreBase with Store { PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress)); + reaction((_) => isAppSecure, (bool isAppSecure) { + sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); + if (Platform.isAndroid) { + setIsAppSecureNative(isAppSecure); + } + }); + + if (Platform.isAndroid) { + setIsAppSecureNative(isAppSecure); + } + reaction( (_) => fiatApiMode, (FiatApiMode mode) => sharedPreferences.setInt( @@ -199,6 +214,9 @@ abstract class SettingsStoreBase with Store { @observable bool shouldSaveRecipientAddress; + @observable + bool isAppSecure; + @observable bool allowBiometricalAuthentication; @@ -289,6 +307,8 @@ abstract class SettingsStoreBase with Store { // FIX-ME: Check for which default value we should have here final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; + final isAppSecure = + sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences .getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); @@ -316,7 +336,7 @@ abstract class SettingsStoreBase with Store { final pinCodeTimeOutDuration = timeOutDuration != null ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) : defaultPinCodeTimeOutDuration; - + // If no value if (pinLength == null || pinLength == 0) { pinLength = defaultPinLength; @@ -367,6 +387,7 @@ abstract class SettingsStoreBase with Store { initialFiatCurrency: currentFiatCurrency, initialBalanceDisplayMode: currentBalanceDisplayMode, initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAppSecure: isAppSecure, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, initialExchangeStatus: exchangeStatus, @@ -412,6 +433,8 @@ abstract class SettingsStoreBase with Store { .getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? shouldSaveRecipientAddress; + isAppSecure = + sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 110afb07a..91ddd2f34 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -21,6 +21,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed FiatApiMode get fiatApiMode => _settingsStore.fiatApiMode; + @computed + bool get isAppSecure => _settingsStore.isAppSecure; + @action void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; @@ -30,4 +33,7 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setFiatMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode; + @action + void setIsAppSecure(bool value) => _settingsStore.isAppSecure = value; + } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 8dbf1b900..1a16e2443 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -699,5 +699,6 @@ "sell_monero_com_alert_content": "بيع Monero غير مدعوم حتى الآن", "error_text_input_below_minimum_limit":" المبلغ أقل من الحد الأدنى", "error_text_input_above_maximum_limit":"المبلغ أكبر من الحد الأقصى", - "show_market_place": "إظهار السوق" + "show_market_place": "إظهار السوق", + "prevent_screenshots": "منع لقطات الشاشة وتسجيل الشاشة" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 2a95efeee..706e37799 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -700,5 +700,6 @@ "sell_monero_com_alert_content": "Продажбата на Monero все още не се поддържа", "error_text_input_below_minimum_limit" : "Сумата е по-малко от минималната", "error_text_input_above_maximum_limit" : "Сумата надвишава максималната", - "show_market_place":"Покажи пазар" + "show_market_place":"Покажи пазар", + "prevent_screenshots": "Предотвратете екранни снимки и запис на екрана" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ddd22eb61..66006fd8b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -700,5 +700,6 @@ "sell_monero_com_alert_content": "Prodej Monero zatím není podporován", "error_text_input_below_minimum_limit" : "Částka je menší než minimální hodnota", "error_text_input_above_maximum_limit" : "Částka je větší než maximální hodnota", - "show_market_place": "Zobrazit trh" + "show_market_place": "Zobrazit trh", + "prevent_screenshots": "Zabránit vytváření snímků obrazovky a nahrávání obrazovky" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d29a641e2..f79fb8b4a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Der Verkauf von Monero wird noch nicht unterstützt", "error_text_input_below_minimum_limit" : "Menge ist unter dem Minimum", "error_text_input_above_maximum_limit" : "Menge ist über dem Maximum", - "show_market_place": "Marktplatz anzeigen" + "show_market_place": "Marktplatz anzeigen", + "prevent_screenshots": "Verhindern Sie Screenshots und Bildschirmaufzeichnungen" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index f4d693e8f..2fe1875c5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Selling Monero is not supported yet", "error_text_input_below_minimum_limit" : "Amount is less than the minimum", "error_text_input_above_maximum_limit" : "Amount is more than the maximum", - "show_market_place" :"Show Marketplace" + "show_market_place" :"Show Marketplace", + "prevent_screenshots": "Prevent screenshots and screen recording" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index cee502fd3..11153e3ff 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Aún no se admite la venta de Monero", "error_text_input_below_minimum_limit" : "La cantidad es menos que mínima", "error_text_input_above_maximum_limit" : "La cantidad es más que el máximo", - "show_market_place": "Mostrar mercado" + "show_market_place": "Mostrar mercado", + "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index c4a44895a..47abb1f2f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "La vente de Monero n'est pas encore prise en charge", "error_text_input_below_minimum_limit" : "Le montant est inférieur au minimum", "error_text_input_above_maximum_limit" : "Le montant est supérieur au maximum", - "show_market_place" :"Afficher la place de marché" + "show_market_place" :"Afficher la place de marché", + "prevent_screenshots": "Empêcher les captures d'écran et l'enregistrement d'écran" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 2669af5dc..d4512c370 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "मोनेरो बेचना अभी तक समर्थित नहीं है", "error_text_input_below_minimum_limit" : "राशि न्यूनतम से कम है", "error_text_input_above_maximum_limit" : "राशि अधिकतम से अधिक है", - "show_market_place":"बाज़ार दिखाएँ" + "show_market_place":"बाज़ार दिखाएँ", + "prevent_screenshots": "स्क्रीनशॉट और स्क्रीन रिकॉर्डिंग रोकें" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 865494c8d..9d5e85fc9 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Prodaja Monera još nije podržana", "error_text_input_below_minimum_limit" : "Iznos je manji od minimalnog", "error_text_input_above_maximum_limit" : "Iznos je veći od maskimalnog", - "show_market_place" : "Prikaži tržište" + "show_market_place" : "Prikaži tržište", + "prevent_screenshots": "Spriječite snimke zaslona i snimanje zaslona" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 12e1f2cfa..4ade62ce8 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -682,5 +682,6 @@ "sell_monero_com_alert_content": "Menjual Monero belum didukung", "error_text_input_below_minimum_limit" : "Jumlah kurang dari minimal", "error_text_input_above_maximum_limit" : "Jumlah lebih dari maksimal", - "show_market_place": "Tampilkan Pasar" + "show_market_place": "Tampilkan Pasar", + "prevent_screenshots": "Cegah tangkapan layar dan perekaman layar" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index dce7df8e3..8cef4e216 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "La vendita di Monero non è ancora supportata", "error_text_input_below_minimum_limit" : "L'ammontare è inferiore al minimo", "error_text_input_above_maximum_limit" : "L'ammontare è superiore al massimo", - "show_market_place":"Mostra mercato" + "show_market_place":"Mostra mercato", + "prevent_screenshots": "Impedisci screenshot e registrazione dello schermo" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 674457116..ac2eb8642 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "モネロの販売はまだサポートされていません", "error_text_input_below_minimum_limit" : "金額は最小額より少ない", "error_text_input_above_maximum_limit" : "金額は最大値を超えています", - "show_market_place":"マーケットプレイスを表示" + "show_market_place":"マーケットプレイスを表示", + "prevent_screenshots": "スクリーンショットと画面録画を防止する" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 4b35b313b..cf829bb46 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "지원되지 않습니다.", "error_text_input_below_minimum_limit" : "금액이 최소보다 적습니다.", "error_text_input_above_maximum_limit" : "금액이 최대 값보다 많습니다.", - "show_market_place":"마켓플레이스 표시" + "show_market_place":"마켓플레이스 표시", + "prevent_screenshots": "스크린샷 및 화면 녹화 방지" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index f08634f48..ac2b056ee 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Monero ရောင်းချခြင်းကို မပံ့ပိုးရသေးပါ။", "error_text_input_below_minimum_limit" : "ပမာဏသည် အနိမ့်ဆုံးထက်နည်းသည်။", "error_text_input_above_maximum_limit" : "ပမာဏသည် အများဆုံးထက် ပိုများသည်။", - "show_market_place":"စျေးကွက်ကိုပြသပါ။" + "show_market_place":"စျေးကွက်ကိုပြသပါ။", + "prevent_screenshots": "ဖန်သားပြင်ဓာတ်ပုံများနှင့် မျက်နှာပြင်ရိုက်ကူးခြင်းကို တားဆီးပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 6e416b004..371df6f62 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Het verkopen van Monero wordt nog niet ondersteund", "error_text_input_below_minimum_limit" : "Bedrag is minder dan minimaal", "error_text_input_above_maximum_limit" : "Bedrag is meer dan maximaal", - "show_market_place":"Toon Marktplaats" + "show_market_place":"Toon Marktplaats", + "prevent_screenshots": "Voorkom screenshots en schermopname" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 357edc2e3..b5e0a0bfd 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Sprzedaż Monero nie jest jeszcze obsługiwana", "error_text_input_below_minimum_limit" : "Kwota jest mniejsza niż minimalna", "error_text_input_above_maximum_limit" : "Kwota jest większa niż maksymalna", - "show_market_place" : "Pokaż rynek" + "show_market_place" : "Pokaż rynek", + "prevent_screenshots": "Zapobiegaj zrzutom ekranu i nagrywaniu ekranu" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index d02147050..633d2e833 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -700,5 +700,6 @@ "sell_monero_com_alert_content": "A venda de Monero ainda não é suportada", "error_text_input_below_minimum_limit" : "O valor é menor que o mínimo", "error_text_input_above_maximum_limit" : "O valor é superior ao máximo", - "show_market_place":"Mostrar mercado" + "show_market_place":"Mostrar mercado", + "prevent_screenshots": "Evite capturas de tela e gravação de tela" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index ad14f4cb0..c9230219c 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Продажа Monero пока не поддерживается", "error_text_input_below_minimum_limit" : "Сумма меньше минимальной", "error_text_input_above_maximum_limit" : "Сумма больше максимальной", - "show_market_place":"Показать торговую площадку" + "show_market_place":"Показать торговую площадку", + "prevent_screenshots": "Предотвратить скриншоты и запись экрана" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b8f229a09..1233ecba4 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -699,5 +699,6 @@ "sell_monero_com_alert_content": "ยังไม่รองรับการขาย Monero", "error_text_input_below_minimum_limit" : "จำนวนเงินน้อยกว่าขั้นต่ำ", "error_text_input_above_maximum_limit" : "จำนวนเงินสูงกว่าค่าสูงสุด", - "show_market_place":"แสดงตลาดกลาง" + "show_market_place":"แสดงตลาดกลาง", + "prevent_screenshots": "ป้องกันภาพหน้าจอและการบันทึกหน้าจอ" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 2ef0b9302..01ba57fbe 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Monero satışı henüz desteklenmiyor", "error_text_input_below_minimum_limit" : "Miktar minimumdan daha azdır", "error_text_input_above_maximum_limit" : "Miktar maksimumdan daha fazla", - "show_market_place":"Pazar Yerini Göster" + "show_market_place":"Pazar Yerini Göster", + "prevent_screenshots": "Ekran görüntülerini ve ekran kaydını önleyin" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 72cccf43b..9d8010eca 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -700,5 +700,6 @@ "sell_monero_com_alert_content": "Продаж Monero ще не підтримується", "error_text_input_below_minimum_limit" : "Сума менша мінімальної", "error_text_input_above_maximum_limit" : "Сума більше максимальної", - "show_market_place":"Шоу Ринок" + "show_market_place":"Відображати маркетплейс", + "prevent_screenshots": "Запобігати знімкам екрана та запису екрана" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 000ce76b4..15ca6833b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -701,5 +701,6 @@ "sell_monero_com_alert_content": "Monero فروخت کرنا ابھی تک تعاون یافتہ نہیں ہے۔", "error_text_input_below_minimum_limit" : "رقم کم از کم سے کم ہے۔", "error_text_input_above_maximum_limit" : "رقم زیادہ سے زیادہ سے زیادہ ہے۔", - "show_market_place":"بازار دکھائیں۔" + "show_market_place":"بازار دکھائیں۔", + "prevent_screenshots": "اسکرین شاٹس اور اسکرین ریکارڈنگ کو روکیں۔" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e95e854d9..d906ebf5b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -700,5 +700,6 @@ "sell_monero_com_alert_content": "尚不支持出售门罗币", "error_text_input_below_minimum_limit" : "金额小于最小值", "error_text_input_above_maximum_limit" : "金额大于最大值", - "show_market_place" :"显示市场" + "show_market_place" :"显示市场", + "prevent_screenshots": "防止截屏和录屏" } From 315c4c911c1c6af214ac3e7a59a1acbb04a19ed5 Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Thu, 20 Apr 2023 16:46:41 +0300 Subject: [PATCH 19/28] CW-325-Coin-Control-enhancements (#846) * fix checkbox * save the output state * add note as a header * Allow copy the Amount and Address * add frozen balance to dashboard * add block explorer * fix url launcher * code formatting * minor fixes * Revert "minor fixes" This reverts commit d230b6a07bc3855407251991926ab0f257c55a5a. * fix missing implementations error * [skip ci] update localization * fix unspent with same txid * add amount check * add vout check * remove formattedTotalAvailableBalance * remove unrelated mac os files --- cw_bitcoin/lib/electrum_balance.dart | 19 +- cw_bitcoin/lib/electrum_wallet.dart | 31 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 2 +- cw_core/lib/unspent_coins_info.dart | 14 +- cw_core/lib/wallet_base.dart | 2 + cw_haven/lib/haven_wallet.dart | 3 + cw_monero/lib/monero_wallet.dart | 2 + .../dashboard/widgets/balance_page.dart | 274 ++++++++++-------- .../unspent_coins_details_page.dart | 32 +- .../widgets/unspent_coins_list_item.dart | 131 ++++----- .../dashboard/balance_view_model.dart | 76 ++++- .../unspent_coins_details_view_model.dart | 76 +++-- .../unspent_coins/unspent_coins_item.dart | 10 +- .../unspent_coins_list_view_model.dart | 75 +++-- res/values/strings_ar.arb | 5 +- res/values/strings_bg.arb | 5 +- res/values/strings_cs.arb | 5 +- res/values/strings_de.arb | 5 +- res/values/strings_en.arb | 5 +- res/values/strings_es.arb | 7 +- res/values/strings_fr.arb | 5 +- res/values/strings_hi.arb | 5 +- res/values/strings_hr.arb | 5 +- res/values/strings_id.arb | 5 +- res/values/strings_it.arb | 5 +- res/values/strings_ja.arb | 5 +- res/values/strings_ko.arb | 5 +- res/values/strings_my.arb | 5 +- res/values/strings_nl.arb | 5 +- res/values/strings_pl.arb | 5 +- res/values/strings_pt.arb | 5 +- res/values/strings_ru.arb | 5 +- res/values/strings_th.arb | 5 +- res/values/strings_tr.arb | 5 +- res/values/strings_uk.arb | 5 +- res/values/strings_ur.arb | 5 +- res/values/strings_zh.arb | 5 +- 37 files changed, 525 insertions(+), 339 deletions(-) diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index a26b79ddb..0a9a33d54 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -4,7 +4,7 @@ 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, required this.frozen}) : super(confirmed, unconfirmed); static ElectrumBalance? fromJSON(String? jsonSource) { @@ -16,20 +16,25 @@ class ElectrumBalance extends Balance { return ElectrumBalance( confirmed: decoded['confirmed'] as int? ?? 0, - unconfirmed: decoded['unconfirmed'] as int? ?? 0); + unconfirmed: decoded['unconfirmed'] as int? ?? 0, + frozen: decoded['frozen'] as int? ?? 0); } final int confirmed; final int unconfirmed; + final int frozen; @override - String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed); + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); @override - String get formattedAdditionalBalance => - bitcoinAmountToString(amount: unconfirmed); + String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); + + String get formattedFrozenBalance { + final frozenFormatted = bitcoinAmountToString(amount: frozen); + return frozenFormatted == '0.0' ? '' : frozenFormatted; + } String toJSON() => - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed}); + json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f31db7bb8..4984ad28e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -63,7 +63,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, _scripthashesUpdateSubject = {}, balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of( currency != null - ? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0)} + ? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, + frozen: 0)} : {}), this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { @@ -133,8 +134,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, await walletAddresses.discoverAddresses(); await updateTransactions(); _subscribeForUpdates(); - await _updateBalance(); await updateUnspent(); + await updateBalance(); _feeRates = await electrumClient.feeRates(); Timer.periodic(const Duration(minutes: 1), @@ -343,7 +344,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, electrumClient: electrumClient, amount: amount, fee: fee) ..addListener((transaction) async { transactionHistory.addOne(transaction); - await _updateBalance(); + await updateBalance(); }); } @@ -497,7 +498,10 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, hash: coin.hash, isFrozen: coin.isFrozen, isSending: coin.isSending, - noteRaw: coin.note + noteRaw: coin.note, + address: coin.address.address, + value: coin.value, + vout: coin.vout, ); await unspentCoinsInfo.add(newInfo); @@ -634,8 +638,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await _updateBalance(); await updateUnspent(); + await updateBalance(); await updateTransactions(); } catch (e) { print(e.toString()); @@ -653,7 +657,17 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, final sh = scriptHash(addressRecord.address, networkType: networkType); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); - } + } + + var totalFrozen = 0; + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.hash == info.hash && info.isFrozen && element.address.address == info.address + && element.value == info.value) { + totalFrozen += element.value; + } + }); + }); final balances = await Future.wait(balanceFutures); var totalConfirmed = 0; @@ -672,10 +686,11 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, } } - return ElectrumBalance(confirmed: totalConfirmed, unconfirmed: totalUnconfirmed); + return ElectrumBalance(confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, + frozen: totalFrozen); } - Future<void> _updateBalance() async { + Future<void> updateBalance() async { balance[currency] = await _fetchBalances(); await save(); } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 3755e7d18..6db0c23f2 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -37,7 +37,7 @@ class ElectrumWallletSnapshot { .map((addr) => BitcoinAddressRecord.fromJSON(addr)) .toList(); final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? - ElectrumBalance(confirmed: 0, unconfirmed: 0); + ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndex = 0; var changeAddressIndex = 0; diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index d825db279..75c13f2cd 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -9,7 +9,10 @@ class UnspentCoinsInfo extends HiveObject { required this.hash, required this.isFrozen, required this.isSending, - required this.noteRaw}); + required this.noteRaw, + required this.address, + required this.vout, + required this.value}); static const typeId = 9; static const boxName = 'Unspent'; @@ -30,6 +33,15 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(4) String? noteRaw; + @HiveField(5, defaultValue: '') + String address; + + @HiveField(6, defaultValue: 0) + int value; + + @HiveField(7, defaultValue: 0) + int vout; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 1983e62b7..93821448c 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -71,4 +71,6 @@ abstract class WalletBase< void close(); Future<void> changePassword(String password); + + Future<void>? updateBalance(); } diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index e761d21fa..2a72f078f 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -104,6 +104,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, (_) async => await save()); } + @override + Future<void>? updateBalance() => null; + @override void close() { _listener?.stop(); diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index cb9cb0ceb..eea490ba9 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -118,6 +118,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, Duration(seconds: _autoSaveInterval), (_) async => await save()); } + @override + Future<void>? updateBalance() => null; @override void close() { diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index bf8b1ae17..14933e7c5 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -8,187 +8,207 @@ 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{ +class BalancePage extends StatelessWidget { BalancePage({required this.dashboardViewModel, required this.settingsStore}); final DashboardViewModel dashboardViewModel; final SettingsStore settingsStore; + Color get backgroundLightColor => + settingsStore.currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; + @override Widget build(BuildContext context) { return GestureDetector( - onLongPress: () => dashboardViewModel.balanceViewModel.isReversing = !dashboardViewModel.balanceViewModel.isReversing, - onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = !dashboardViewModel.balanceViewModel.isReversing, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: ResponsiveLayoutUtil.instance.isMobile(context) ? 56 : 16), + onLongPress: () => dashboardViewModel.balanceViewModel.isReversing = + !dashboardViewModel.balanceViewModel.isReversing, + onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = + !dashboardViewModel.balanceViewModel.isReversing, + child: SingleChildScrollView( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 56), Container( - margin: const EdgeInsets.only(left: 24, bottom: 16), - child: Observer(builder: (_) { - return Text( - dashboardViewModel.balanceViewModel.asset, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, - height: 1), - maxLines: 1, - textAlign: TextAlign.center); - })), + margin: const EdgeInsets.only(left: 24, bottom: 16), + child: Observer(builder: (_) { + return Text(dashboardViewModel.balanceViewModel.asset, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1), + maxLines: 1, + textAlign: TextAlign.center); + })), Observer(builder: (_) { - if (dashboardViewModel.balanceViewModel.isShowCard){ + if (dashboardViewModel.balanceViewModel.isShowCard) { return IntroducingCard( - title: S.of(context).introducing_cake_pay, + 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 - ); + closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); } - return Container (); + return Container(); }), Observer(builder: (_) { return ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), - itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, - itemBuilder: (__, index) { - final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); - return buildBalanceRow(context, - availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), + itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, + itemBuilder: (__, index) { + final balance = + dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); + return buildBalanceRow(context, + availableBalanceLabel: + '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalance: balance.availableBalance, availableFiatBalance: balance.fiatAvailableBalance, - additionalBalanceLabel: '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', + additionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', additionalBalance: balance.additionalBalance, additionalFiatBalance: balance.fiatAdditionalBalance, + frozenBalance: balance.frozenBalance, + frozenFiatBalance: balance.fiatFrozenBalance, currency: balance.formattedAssetTitle); - }); - }) - ]))); + }); + }) + ]))); } Widget buildBalanceRow(BuildContext context, - {required String availableBalanceLabel, + {required String availableBalanceLabel, required String availableBalance, required String availableFiatBalance, required String additionalBalanceLabel, required String additionalBalance, required String additionalFiatBalance, + required String frozenBalance, + required String frozenFiatBalance, required String currency}) { - return Container( + return Container( margin: const EdgeInsets.only(left: 16, right: 16), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all(color: settingsStore.currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), - color:Theme.of(context).textTheme!.headline6!.backgroundColor! - ), + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: settingsStore.currentTheme.type == ThemeType.bright + ? Color.fromRGBO(255, 255, 255, 0.2) + : Colors.transparent, + width: 1, + ), + color: Theme.of(context).textTheme!.headline6!.backgroundColor!), child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 4,), - Text('${availableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( + margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 4, + ), + Text('${availableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor!, + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!, height: 1)), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AutoSizeText( - availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, - height: 1), - maxLines: 1, - textAlign: TextAlign.center), - Text(currency, - style: TextStyle( - fontSize: 28, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, - height: 1)), - ]), - SizedBox(height: 4,), - Text('${availableFiatBalance}', + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + AutoSizeText(availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1), + maxLines: 1, + textAlign: TextAlign.center), + Text(currency, + style: TextStyle( + fontSize: 28, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1)), + ]), + SizedBox( + height: 4, + ), + Text('${availableFiatBalance}', textAlign: TextAlign.center, style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, - height: 1)), - SizedBox(height: 26), - Text('${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor!, - height: 1)), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1)), + SizedBox(height: 26), + if (frozenBalance.isNotEmpty) + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(S.current.frozen_balance, + textAlign: TextAlign.center, style: TextStyle( - fontSize: 20, + fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!, + height: 1)), + SizedBox(height: 8), + AutoSizeText(frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, height: 1), maxLines: 1, textAlign: TextAlign.center), - SizedBox(height: 4,), - Text('${additionalFiatBalance}', + SizedBox(height: 4), + Text( + frozenFiatBalance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1), + ), + SizedBox(height: 24) + ]), + Text('${additionalBalanceLabel}', textAlign: TextAlign.center, style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!, + height: 1)), + SizedBox(height: 8), + AutoSizeText(additionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + height: 1), + maxLines: 1, + textAlign: TextAlign.center), + SizedBox( + height: 4, + ), + Text( + '${additionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .headline2! - .backgroundColor!, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, height: 1), - ) - ])), + ) + ])), ); } } - diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index d8ce24d88..00c7b9796 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -1,13 +1,16 @@ +import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_switch_item.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -30,9 +33,13 @@ class UnspentCoinsDetailsPage extends BasePage { final item = unspentCoinsDetailsViewModel.items[index]; if (item is StandartListItem) { - return ListRow( - title: '${item.title}:', - value: item.value); + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + showBar<void>(context, S.of(context).transaction_details_copied(item.title)); + }, + child: ListRow(title: '${item.title}:', value: item.value), + ); } if (item is TextFieldListItem) { @@ -44,14 +51,21 @@ class UnspentCoinsDetailsPage extends BasePage { } if (item is UnspentCoinsSwitchItem) { - return Observer(builder: (_) => UnspentCoinsSwitchRow( - title: item.title, - switchValue: item.switchValue(), - onSwitchValueChange: item.onSwitchValueChange - )); + return Observer( + builder: (_) => UnspentCoinsSwitchRow( + title: item.title, + switchValue: item.switchValue(), + onSwitchValueChange: item.onSwitchValueChange)); + } + + if (item is BlockExplorerListItem) { + return GestureDetector( + onTap: item.onTap, + child: ListRow(title: '${item.title}:', value: item.value), + ); } return Container(); }); } -} \ No newline at end of file +} diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index fac036f0e..b1916d06c 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -1,5 +1,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -28,99 +29,77 @@ class UnspentCoinsListItem extends StatelessWidget { @override Widget build(BuildContext context) { - final itemColor = isSending? selectedItemColor : unselectedItemColor; - final _note = (note?.isNotEmpty ?? false) ? note : address; - + final itemColor = isSending ? selectedItemColor : unselectedItemColor; return Container( - height: 62, - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(12)), - color: itemColor), + height: 70, + padding: EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: + BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(12)), color: itemColor), child: Row( - mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: EdgeInsets.only(right: 12), - child: GestureDetector( - onTap: () => onCheckBoxTap?.call(), - child: Container( - height: 24.0, - width: 24.0, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .primaryTextTheme! - .caption! - .color!, - width: 1.0), - borderRadius: BorderRadius.all( - Radius.circular(8.0)), - color: itemColor), - child: isSending - ? Icon( - Icons.check, - color: Colors.blue, - size: 20.0, - ) - : Offstage(), - ) - ) - ), + child: StandardCheckbox( + value: isSending, onChanged: (value) => onCheckBoxTap?.call())), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: AutoSizeText( - amount, - style: TextStyle( - color: amountColor, - fontSize: 16, - fontWeight: FontWeight.w600 - ), - maxLines: 1, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (note.isNotEmpty) + AutoSizeText( + note, + style: TextStyle( + color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), + maxLines: 1, ), - if (isFrozen) Container( + AutoSizeText( + amount, + style: + TextStyle(color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), + maxLines: 1, + ) + ]), + if (isFrozen) + Container( height: 17, padding: EdgeInsets.only(left: 6, right: 6), decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), alignment: Alignment.center, child: Text( - S.of(context).frozen, - style: TextStyle( - color: amountColor, - fontSize: 7, - fontWeight: FontWeight.w600 - ), - ) - ) - ], - ), - Text( - _note, - style: TextStyle( - color: addressColor, - fontSize: 12, + S.of(context).frozen, + style: + TextStyle(color: amountColor, fontSize: 7, fontWeight: FontWeight.w600), + )) + ], + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AutoSizeText( + address, + style: TextStyle( + color: addressColor, + fontSize: 12, + ), + maxLines: 1, ), - maxLines: 1, - overflow: TextOverflow.ellipsis - ) - ] - ) - ) + ], + ), + ), + ])), ], - ) - ); + )); } -} \ No newline at end of file +} diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 33fe01ba9..3b9dcce55 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,5 +1,9 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -11,6 +15,7 @@ import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; @@ -19,14 +24,18 @@ class BalanceRecord { const BalanceRecord({ required this.availableBalance, required this.additionalBalance, + required this.frozenBalance, required this.fiatAvailableBalance, required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, required this.asset, required this.formattedAssetTitle}); final String fiatAdditionalBalance; final String fiatAvailableBalance; + final String fiatFrozenBalance; final String additionalBalance; final String availableBalance; + final String frozenBalance; final CryptoCurrency asset; final String formattedAssetTitle; } @@ -135,6 +144,32 @@ abstract class BalanceViewModelBase with Store { return walletBalance.formattedAvailableBalance; } + @computed + String get frozenBalance { + final walletBalance = _walletBalance; + + if (displayMode == BalanceDisplayMode.hiddenBalance) { + return '---'; + } + + return getFormattedFrozenBalance(walletBalance); + } + + @computed + String get frozenFiatBalance { + final walletBalance = _walletBalance; + final fiatCurrency = settingsStore.fiatCurrency; + + if (displayMode == BalanceDisplayMode.hiddenBalance) { + return '---'; + } + + return _getFiatBalance( + price: price, + cryptoAmount: getFormattedFrozenBalance(walletBalance)) + ' ' + fiatCurrency.toString(); + + } + @computed String get additionalBalance { final walletBalance = _walletBalance; @@ -158,7 +193,7 @@ abstract class BalanceViewModelBase with Store { return _getFiatBalance( price: price, cryptoAmount: walletBalance.formattedAvailableBalance) + ' ' + fiatCurrency.toString(); - + } @computed @@ -173,7 +208,7 @@ abstract class BalanceViewModelBase with Store { return _getFiatBalance( price: price, cryptoAmount: walletBalance.formattedAdditionalBalance) + ' ' + fiatCurrency.toString(); - + } @computed @@ -183,8 +218,10 @@ abstract class BalanceViewModelBase with Store { return MapEntry(key, BalanceRecord( availableBalance: '---', additionalBalance: '---', + frozenBalance: '---', fiatAdditionalBalance: isFiatDisabled ? '' : '---', fiatAvailableBalance: isFiatDisabled ? '' : '---', + fiatFrozenBalance: isFiatDisabled ? '' : '---', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -207,13 +244,25 @@ abstract class BalanceViewModelBase with Store { price: price, cryptoAmount: value.formattedAvailableBalance)); - return MapEntry(key, BalanceRecord( - availableBalance: value.formattedAvailableBalance, - additionalBalance: value.formattedAdditionalBalance, - fiatAdditionalBalance: additionalFiatBalance, - fiatAvailableBalance: availableFiatBalance, - asset: key, - formattedAssetTitle: _formatterAsset(key))); + + final frozenFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() + + ' ' + + _getFiatBalance( + price: price, + cryptoAmount: getFormattedFrozenBalance(value))); + + + return MapEntry( + key, + BalanceRecord( + availableBalance: value.formattedAvailableBalance, + additionalBalance: value.formattedAdditionalBalance, + frozenBalance: getFormattedFrozenBalance(value), + fiatAdditionalBalance: additionalFiatBalance, + fiatAvailableBalance: availableFiatBalance, + fiatFrozenBalance: frozenFiatBalance, + asset: key, + formattedAssetTitle: _formatterAsset(key))); }); } @@ -290,7 +339,7 @@ abstract class BalanceViewModelBase with Store { } String _getFiatBalance({required double price, String? cryptoAmount}) { - if (cryptoAmount == null) { + if (cryptoAmount == null || cryptoAmount.isEmpty) { return '0.00'; } @@ -306,10 +355,15 @@ abstract class BalanceViewModelBase with Store { return assetStringified.replaceFirst('X', 'x'); } - return asset.toString(); + return asset.toString(); default: return asset.toString(); } } + + + String getFormattedFrozenBalance(Balance walletBalance) => + walletBalance is ElectrumBalance ? walletBalance.formattedFrozenBalance : ''; + } diff --git a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart index ed8687c87..098296036 100644 --- a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; @@ -5,7 +6,9 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_switch_item.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; +import 'package:url_launcher/url_launcher.dart'; part 'unspent_coins_details_view_model.g.dart'; @@ -13,21 +16,14 @@ class UnspentCoinsDetailsViewModel = UnspentCoinsDetailsViewModelBase with _$UnspentCoinsDetailsViewModel; abstract class UnspentCoinsDetailsViewModelBase with Store { - UnspentCoinsDetailsViewModelBase({ - required this.unspentCoinsItem, - required this.unspentCoinsListViewModel}) + UnspentCoinsDetailsViewModelBase( + {required this.unspentCoinsItem, required this.unspentCoinsListViewModel}) : items = <TransactionDetailsListItem>[], isFrozen = unspentCoinsItem.isFrozen, note = unspentCoinsItem.note { items = [ - StandartListItem( - title: S.current.transaction_details_amount, - value: unspentCoinsItem.amount - ), - StandartListItem( - title: S.current.widgets_address, - value: unspentCoinsItem.address - ), + StandartListItem(title: S.current.transaction_details_amount, value: unspentCoinsItem.amount), + StandartListItem(title: S.current.widgets_address, value: unspentCoinsItem.address), TextFieldListItem( title: S.current.note_tap_to_change, value: note, @@ -36,21 +32,53 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { unspentCoinsListViewModel.saveUnspentCoinInfo(unspentCoinsItem); }), UnspentCoinsSwitchItem( - title: S.current.freeze, - value: '', - switchValue: () => isFrozen, - onSwitchValueChange: (value) async { - isFrozen = value; - unspentCoinsItem.isFrozen = value; - if (value) { - unspentCoinsItem.isSending = !value; - } - await unspentCoinsListViewModel.saveUnspentCoinInfo(unspentCoinsItem); - } - ) + title: S.current.freeze, + value: '', + switchValue: () => isFrozen, + onSwitchValueChange: (value) async { + isFrozen = value; + unspentCoinsItem.isFrozen = value; + if (value) { + unspentCoinsItem.isSending = !value; + } + await unspentCoinsListViewModel.saveUnspentCoinInfo(unspentCoinsItem); + }), + BlockExplorerListItem( + title: S.current.view_in_block_explorer, + value: _explorerDescription(unspentCoinsListViewModel.wallet.type), + onTap: () { + try { + final url = Uri.parse( + _explorerUrl(unspentCoinsListViewModel.wallet.type, unspentCoinsItem.hash)); + return launchUrl(url); + } catch (e) {} + + }) ]; } + String _explorerUrl(WalletType type, String txId) { + switch (type) { + case WalletType.bitcoin: + return 'https://ordinals.com/tx/${txId}'; + case WalletType.litecoin: + return 'https://litecoin.earlyordies.com/tx/${txId}'; + default: + return ''; + } + } + + String _explorerDescription(WalletType type) { + switch (type) { + case WalletType.bitcoin: + return S.current.view_transaction_on + 'Ordinals.com'; + case WalletType.litecoin: + return S.current.view_transaction_on + 'Earlyordies.com'; + default: + return ''; + } + } + @observable bool isFrozen; @@ -60,4 +88,4 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { final UnspentCoinsItem unspentCoinsItem; final UnspentCoinsListViewModel unspentCoinsListViewModel; List<TransactionDetailsListItem> items; -} \ No newline at end of file +} diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index c6b9eb375..2f0d75571 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -11,7 +11,9 @@ abstract class UnspentCoinsItemBase with Store { required this.hash, required this.isFrozen, required this.note, - required this.isSending}); + required this.isSending, + required this.amountRaw, + required this.vout}); @observable String address; @@ -30,4 +32,10 @@ abstract class UnspentCoinsItemBase with Store { @observable bool isSending; + + @observable + int amountRaw; + + @observable + int vout; } \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index dbd43fdac..4df4b9a66 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,60 +1,81 @@ -//import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -//import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; -import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; +import 'package:collection/collection.dart'; part 'unspent_coins_list_view_model.g.dart'; class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; abstract class UnspentCoinsListViewModelBase with Store { - UnspentCoinsListViewModelBase({ - required this.wallet, - required Box<UnspentCoinsInfo> unspentCoinsInfo}) - : _unspentCoinsInfo = unspentCoinsInfo { + UnspentCoinsListViewModelBase( + {required this.wallet, required Box<UnspentCoinsInfo> unspentCoinsInfo}) + : _unspentCoinsInfo = unspentCoinsInfo { bitcoin!.updateUnspents(wallet); } WalletBase wallet; - Box<UnspentCoinsInfo> _unspentCoinsInfo; + final Box<UnspentCoinsInfo> _unspentCoinsInfo; @computed - ObservableList<UnspentCoinsItem> get items => ObservableList.of(bitcoin!.getUnspents(wallet).map((elem) { - final amount = bitcoin!.formatterBitcoinAmountToString(amount: elem.value) + - ' ${wallet.currency.title}'; - - final info = _unspentCoinsInfo.values - .firstWhere((element) => element.walletId == wallet.id && element.hash == elem.hash); + ObservableList<UnspentCoinsItem> get items => + ObservableList.of(bitcoin!.getUnspents(wallet).map((elem) { + final amount = bitcoin!.formatterBitcoinAmountToString(amount: elem.value) + + ' ${wallet.currency.title}'; - return UnspentCoinsItem( - address: elem.address, - amount: amount, - hash: elem.hash, - isFrozen: elem.isFrozen, - note: info.note, - isSending: elem.isSending - ); - })); + final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout); + + return UnspentCoinsItem( + address: elem.address, + amount: amount, + hash: elem.hash, + isFrozen: info?.isFrozen ?? false, + note: info?.note ?? '', + isSending: info?.isSending ?? true, + amountRaw: elem.value, + vout: elem.vout); + })); Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async { try { - final info = _unspentCoinsInfo.values - .firstWhere((element) => element.walletId.contains(wallet.id) && - element.hash.contains(item.hash)); + final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout); + if (info == null) { + final newInfo = UnspentCoinsInfo( + walletId: wallet.id, + hash: item.hash, + address: item.address, + value: item.amountRaw, + vout: item.vout, + isFrozen: item.isFrozen, + isSending: item.isSending, + noteRaw: item.note); + await _unspentCoinsInfo.add(newInfo); + bitcoin!.updateUnspents(wallet); + wallet.updateBalance(); + return; + } info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; await info.save(); bitcoin!.updateUnspents(wallet); + wallet.updateBalance(); } catch (e) { print(e.toString()); } } -} \ No newline at end of file + + UnspentCoinsInfo? getUnspentCoinInfo(String hash, String address, int value, int vout) { + return _unspentCoinsInfo.values.firstWhereOrNull((element) => + element.walletId == wallet.id && + element.hash == hash && + element.address == address && + element.value == value && + element.vout == vout); + } +} diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 1a16e2443..23686e451 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -479,8 +479,8 @@ "xrp_extra_info":"من فضلك لا تنس تحديد علامة الوجهة أثناء إرسال معاملة XRP للتبادل", "exchange_incorrect_current_wallet_for_xmr":"إذا كنت ترغب في استبدال XMR من رصيد Cake Wallet Monero ، فيرجى التبديل إلى محفظة Monero أولاً.", - "confirmed":"مؤكد", - "unconfirmed":"غير مؤكد", + "confirmed":"رصيد مؤكد", + "unconfirmed":"رصيد غير مؤكد", "displayable":"قابل للعرض", "submit_request":"تقديم طلب", @@ -685,6 +685,7 @@ "error_dialog_content": "عفوًا ، لقد حصلنا على بعض الخطأ.\n\nيرجى إرسال تقرير التعطل إلى فريق الدعم لدينا لتحسين التطبيق.", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "edit_node": "تحرير العقدة", + "frozen_balance": "الرصيد المجمد", "invoice_details": "تفاصيل الفاتورة", "donation_link_details": "تفاصيل رابط التبرع", "anonpay_description": "توليد ${type}. يمكن للمستلم ${method} بأي عملة مشفرة مدعومة ، وستتلقى أموالاً في هذه", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 706e37799..66a211ed9 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Не забравяйте да дадете Destination Tag-а, когато изпращате XRP транзакцията за обмена", "exchange_incorrect_current_wallet_for_xmr" : "Ако искате да обмените XMR от своя Cake Wallet Monero баланс, първо изберете своя Monero портфейл.", - "confirmed" : "Потвърдено", - "unconfirmed" : "Непотвърдено", + "confirmed" : "Потвърден баланс", + "unconfirmed" : "Непотвърден баланс", "displayable" : "Възможност за показване", "submit_request" : "изпращане на заявка", @@ -687,6 +687,7 @@ "error_dialog_content": "Получихме грешка.\n\nМоля, изпратете доклада до нашия отдел поддръжка, за да подобрим приложението.", "decimal_places_error": "Твърде много знаци след десетичната запетая", "edit_node": "Редактиране на възел", + "frozen_balance": "Замразен баланс", "invoice_details": "IДанни за фактура", "donation_link_details": "Подробности за връзката за дарение", "anonpay_description": "Генерирайте ${type}. Получателят може да ${method} с всяка поддържана криптовалута и вие ще получите средства в този портфейл.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 66006fd8b..381e7c18e 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Prosím nezapomeňte zadat Destination Tag, když posíláte XRP transakce ke směně", "exchange_incorrect_current_wallet_for_xmr" : "Pokud chcete směnit XMR z Monero částky v Cake Wallet, prosím přepněte se nejprve do své Monero peněženky.", - "confirmed" : "Potvrzeno", - "unconfirmed" : "Nepotvrzeno", + "confirmed" : "Potvrzený zůstatek", + "unconfirmed" : "Nepotvrzený zůstatek", "displayable" : "Zobrazitelné", "submit_request" : "odeslat požadavek", @@ -687,6 +687,7 @@ "error_dialog_content": "Nastala chyba.\n\nProsím odešlete zprávu o chybě naší podpoře, aby mohli zajistit opravu.", "decimal_places_error": "Příliš mnoho desetinných míst", "edit_node": "Upravit uzel", + "frozen_balance": "Zmrazená bilance", "invoice_details": "detaily faktury", "donation_link_details": "Podrobnosti odkazu na darování", "anonpay_description": "Vygenerujte ${type}. Příjemce může ${method} s jakoukoli podporovanou kryptoměnou a vy obdržíte prostředky v této peněžence.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index f79fb8b4a..9fd3432bd 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Bitte vergessen Sie nicht, das Ziel-Tag anzugeben, während Sie die XRP-Transaktion für den Austausch senden", "exchange_incorrect_current_wallet_for_xmr" : "Wenn Sie XMR von Ihrem Cake Wallet Monero-Guthaben umtauschen möchten, wechseln Sie bitte zuerst zu Ihrer Monero-Wallet.", - "confirmed" : "Bestätigt", - "unconfirmed" : "Unbestätigt", + "confirmed" : "Bestätigter Saldo", + "unconfirmed" : "Unbestätigter Saldo", "displayable" : "Anzeigebar", "submit_request" : "Eine Anfrage stellen", @@ -687,6 +687,7 @@ "error_dialog_content": "Hoppla, wir haben einen Fehler.\n\nBitte senden Sie den Absturzbericht an unser Support-Team, um die Anwendung zu verbessern.", "decimal_places_error": "Zu viele Nachkommastellen", "edit_node": "Knoten bearbeiten", + "frozen_balance": "Gefrorenes Gleichgewicht", "invoice_details": "Rechnungs-Details", "donation_link_details": "Details zum Spendenlink", "anonpay_description": "Generieren Sie ${type}. Der Empfänger kann ${method} mit jeder unterstützten Kryptowährung verwenden, und Sie erhalten Geld in dieser Brieftasche.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 2fe1875c5..7f03b96e9 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Please don’t forget to specify the Destination Tag while sending the XRP transaction for the exchange", "exchange_incorrect_current_wallet_for_xmr" : "If you want to exchange XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", - "confirmed" : "Confirmed", - "unconfirmed" : "Unconfirmed", + "confirmed" : "Confirmed Balance", + "unconfirmed" : "Unconfirmed Balance", "displayable" : "Displayable", "submit_request" : "submit a request", @@ -697,6 +697,7 @@ "onion_link": "Onion link", "decimal_places_error": "Too many decimal places", "edit_node": "Edit Node", + "frozen_balance": "Frozen Balance", "settings": "Settings", "sell_monero_com_alert_content": "Selling Monero is not supported yet", "error_text_input_below_minimum_limit" : "Amount is less than the minimum", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 11153e3ff..4b649e12a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "No olvide especificar la etiqueta de destino al enviar la transacción XRP para el intercambio", "exchange_incorrect_current_wallet_for_xmr" : "Si desea intercambiar XMR de su saldo de Cake Wallet Monero, primero cambie a su billetera Monero.", - "confirmed" : "Confirmada", - "unconfirmed" : "Inconfirmado", + "confirmed" : "Saldo confirmado", + "unconfirmed" : "Saldo no confirmado", "displayable" : "Visualizable", "submit_request" : "presentar una solicitud", @@ -686,7 +686,8 @@ "do_not_send": "no enviar", "error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.", "decimal_places_error": "Demasiados lugares decimales", - "edit_node": "Edit Node", + "edit_node": "Editar nodo", + "frozen_balance": "Balance congelado", "invoice_details": "Detalles de la factura", "donation_link_details": "Detalles del enlace de donación", "anonpay_description": "Genera ${type}. El destinatario puede ${method} con cualquier criptomoneda admitida, y recibirá fondos en esta billetera.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 47abb1f2f..5250ed88d 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -479,8 +479,8 @@ "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 (wallet) Monero au préalable.", - "confirmed" : "Confirmé", - "unconfirmed" : "Non confirmé", + "confirmed" : "Solde confirmé", + "unconfirmed" : "Solde non confirmé", "displayable" : "Visible", "submit_request" : "soumettre une requête", @@ -687,6 +687,7 @@ "error_dialog_content": "Oups, nous avons rencontré une erreur.\n\nMerci d'envoyer le rapport d'erreur à notre équipe d'assistance afin de nous permettre d'améliorer l'application.", "decimal_places_error": "Trop de décimales", "edit_node": "Modifier le nœud", + "frozen_balance": "Équilibre gelé", "invoice_details": "Détails de la facture", "donation_link_details": "Détails du lien de don", "anonpay_description": "Générez ${type}. Le destinataire peut ${method} avec n'importe quelle crypto-monnaie prise en charge, et vous recevrez des fonds dans ce portefeuille (wallet).", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index d4512c370..6a9a97880 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "एक्सचेंज के लिए एक्सआरपी लेनदेन भेजते समय कृपया गंतव्य टैग निर्दिष्ट करना न भूलें", "exchange_incorrect_current_wallet_for_xmr" : "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से एक्सएमआर का आदान-प्रदान करना चाहते हैं, तो कृपया अपने मोनेरो वॉलेट में जाएं।", - "confirmed" : "की पुष्टि की", - "unconfirmed" : "अपुष्ट", + "confirmed" : "पुष्टि की गई शेष राशिी", + "unconfirmed" : "अपुष्ट शेष राशि", "displayable" : "प्रदर्शन योग्य", "submit_request" : "एक अनुरोध सबमिट करें", @@ -687,6 +687,7 @@ "error_dialog_content": "ओह, हमसे कुछ गड़बड़ी हुई है.\n\nएप्लिकेशन को बेहतर बनाने के लिए कृपया क्रैश रिपोर्ट हमारी सहायता टीम को भेजें।", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "edit_node": "नोड संपादित करें", + "frozen_balance": "जमे हुए संतुलन", "invoice_details": "चालान विवरण", "donation_link_details": "दान लिंक विवरण", "anonpay_description": "${type} उत्पन्न करें। प्राप्तकर्ता किसी भी समर्थित क्रिप्टोकरेंसी के साथ ${method} कर सकता है, और आपको इस वॉलेट में धन प्राप्त होगा।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9d5e85fc9..10974efa2 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Molimo ne zaboravite navesti odredišnu oznaku prilikom slanja XRP transakcije na razmjenu", "exchange_incorrect_current_wallet_for_xmr" : "Ako želite razmijeniti XMR s vlastitog Monero računa na Cake Wallet novčaniku, molimo prvo se prebacite na svoj Monero novčanik.", - "confirmed" : "Potvrđeno", - "unconfirmed" : "Nepotvrđeno", + "confirmed" : "Potvrđeno stanje", + "unconfirmed" : "Nepotvrđeno stanje", "displayable" : "Dostupno za prikaz", "submit_request" : "podnesi zahtjev", @@ -687,6 +687,7 @@ "error_dialog_content": "Ups, imamo grešku.\n\nPošaljite izvješće o padu našem timu za podršku kako bismo poboljšali aplikaciju.", "decimal_places_error": "Previše decimalnih mjesta", "edit_node": "Uredi čvor", + "frozen_balance": "Zamrznuti saldo", "invoice_details": "Podaci o fakturi", "donation_link_details": "Detalji veza za donacije", "anonpay_description": "Generiraj ${type}. Primatelj može ${method} s bilo kojom podržanom kriptovalutom, a vi ćete primiti sredstva u ovaj novčanik.", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 4ade62ce8..b4dd13356 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -466,8 +466,8 @@ "xrp_extra_info" : "Jangan lupa untuk menentukan Tag Tujuan saat mengirim transaksi XRP untuk pertukaran", "exchange_incorrect_current_wallet_for_xmr" : "Jika Anda ingin menukar XMR dari saldo Monero Cake Wallet Anda, silakan beralih ke dompet Monero Anda terlebih dahulu.", - "confirmed" : "Dikonfirmasi", - "unconfirmed" : "Tidak dikonfirmasi", + "confirmed" : "Saldo Terkonfirmasi", + "unconfirmed" : "Saldo Belum Dikonfirmasi", "displayable" : "Dapat ditampilkan", "submit_request" : "kirim permintaan", @@ -669,6 +669,7 @@ "contact_list_wallets": "Dompet Saya", "decimal_places_error": "Terlalu banyak tempat desimal", "edit_node": "Sunting Node", + "frozen_balance": "Saldo Beku", "invoice_details": "Detail faktur", "donation_link_details": "Detail tautan donasi", "anonpay_description": "Hasilkan ${type}. Penerima dapat ${method} dengan cryptocurrency apa pun yang didukung, dan Anda akan menerima dana di dompet ini.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8cef4e216..b8881e2f6 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Gentilmente ricorda di indicare il Tag di Destinazione quando invii una transazione XRP per lo scambio", "exchange_incorrect_current_wallet_for_xmr" : "Se vuoi scambiare XMR dal tuo saldo Cake Wallet Monero, gentilmente passa al tuo portafoglio Monero.", - "confirmed" : "Confermato", - "unconfirmed" : "Non confermato", + "confirmed" : "Saldo confermato", + "unconfirmed" : "Saldo non confermato", "displayable" : "Visualizzabile", "submit_request" : "invia una richiesta", @@ -687,6 +687,7 @@ "error_dialog_content": "Spiacenti, abbiamo riscontrato un errore.\n\nSi prega di inviare il rapporto sull'arresto anomalo al nostro team di supporto per migliorare l'applicazione.", "decimal_places_error": "Troppe cifre decimali", "edit_node": "Modifica nodo", + "frozen_balance": "Equilibrio congelato", "invoice_details": "Dettagli della fattura", "donation_link_details": "Dettagli del collegamento alla donazione", "anonpay_description": "Genera ${type}. Il destinatario può ${method} con qualsiasi criptovaluta supportata e riceverai fondi in questo portafoglio.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index ac2eb8642..97baf1281 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "取引所のXRPトランザクションを送信するときに、宛先タグを指定することを忘れないでください", "exchange_incorrect_current_wallet_for_xmr" : "Cake Wallet Moneroの残高からXMRを交換する場合は、最初にMoneroウォレットに切り替えてください。", - "confirmed" : "確認済み", - "unconfirmed" : "未確認", + "confirmed" : "確認済み残高", + "unconfirmed" : "残高未確認", "displayable" : "表示可能", "submit_request" : "リクエストを送信する", @@ -687,6 +687,7 @@ "error_dialog_content": "エラーが発生しました。\n\nアプリケーションを改善するために、クラッシュ レポートをサポート チームに送信してください。", "decimal_places_error": "小数点以下の桁数が多すぎる", "edit_node": "ノードを編集", + "frozen_balance": "冷凍残高", "invoice_details": "請求の詳細", "donation_link_details": "寄付リンクの詳細", "anonpay_description": "${type} を生成します。受取人はサポートされている任意の暗号通貨で ${method} でき、あなたはこのウォレットで資金を受け取ります。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index cf829bb46..50994a752 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "교환을 위해 XRP 트랜잭션을 보내는 동안 대상 태그를 지정하는 것을 잊지 마십시오", "exchange_incorrect_current_wallet_for_xmr" : "Cake Wallet Monero 잔액에서 XMR을 교환하려면 먼저 Monero 지갑으로 전환하십시오.", - "confirmed" : "확인", - "unconfirmed" : "미확인", + "confirmed" : "확인된 잔액", + "unconfirmed" : "확인되지 않은 잔액", "displayable" : "표시 가능", "submit_request" : "요청을 제출", @@ -687,6 +687,7 @@ "error_dialog_content": "죄송합니다. 오류가 발생했습니다.\n\n응용 프로그램을 개선하려면 지원 팀에 충돌 보고서를 보내주십시오.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "edit_node": "노드 편집", + "frozen_balance": "얼어붙은 균형", "invoice_details": "인보이스 세부정보", "donation_link_details": "기부 링크 세부정보", "anonpay_description": "${type} 생성. 수신자는 지원되는 모든 암호화폐로 ${method}할 수 있으며 이 지갑에서 자금을 받게 됩니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index ac2b056ee..e93b8a0b9 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "လဲလှယ်မှုအတွက် XRP ငွေလွှဲပို့နေစဉ် Destination Tag ကို သတ်မှတ်ရန် မမေ့ပါနှင့်", "exchange_incorrect_current_wallet_for_xmr" : "သင်၏ Cake Wallet Monero လက်ကျန်မှ XMR ကိုလဲလှယ်လိုပါက၊ သင်၏ Monero ပိုက်ဆံအိတ်သို့ ဦးစွာပြောင်းပါ။", - "confirmed" : "အတည်ပြုခဲ့သည်။", - "unconfirmed" : "အတည်မပြုနိုင်ပါ။", + "confirmed" : "အတည်ပြုထားသော လက်ကျန်ငွေ", + "unconfirmed" : "အတည်မပြုနိုင်သော လက်ကျန်ငွေ", "displayable" : "ပြသနိုင်သည်။", "submit_request" : "တောင်းဆိုချက်တစ်ခုတင်ပြပါ။", @@ -687,6 +687,7 @@ "error_dialog_content": "အိုး၊ ကျွန်ုပ်တို့တွင် အမှားအယွင်းအချို့ရှိသည်။\n\nအပလီကေးရှင်းကို ပိုမိုကောင်းမွန်စေရန်အတွက် ပျက်စီးမှုအစီရင်ခံစာကို ကျွန်ုပ်တို့၏ပံ့ပိုးကူညီရေးအဖွဲ့ထံ ပေးပို့ပါ။", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "edit_node": "Node ကို တည်းဖြတ်ပါ။", + "frozen_balance": "ေးခဲမှူ", "invoice_details": "ပြေစာအသေးစိတ်", "donation_link_details": "လှူဒါန်းရန်လင့်ခ်အသေးစိတ်", "anonpay_description": "${type} ကို ဖန်တီးပါ။ လက်ခံသူက ${method} ကို ပံ့ပိုးပေးထားသည့် cryptocurrency တစ်ခုခုဖြင့် လုပ်ဆောင်နိုင်ပြီး၊ သင်သည် ဤပိုက်ဆံအိတ်တွင် ရံပုံငွေများ ရရှိမည်ဖြစ်သည်။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 371df6f62..1561bb3d4 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Vergeet niet om de Destination Tag op te geven tijdens het verzenden van de XRP-transactie voor de uitwisseling", "exchange_incorrect_current_wallet_for_xmr" : "Als u XMR wilt omwisselen van uw Cake Wallet Monero-saldo, moet u eerst overschakelen naar uw Monero-portemonnee.", - "confirmed" : "Bevestigd", - "unconfirmed" : "Niet bevestigd", + "confirmed" : "Bevestigd saldo", + "unconfirmed" : "Onbevestigd saldo", "displayable" : "Weer te geven", "submit_request" : "een verzoek indienen", @@ -687,6 +687,7 @@ "error_dialog_content": "Oeps, er is een fout opgetreden.\n\nStuur het crashrapport naar ons ondersteuningsteam om de applicatie te verbeteren.", "decimal_places_error": "Te veel decimalen", "edit_node": "Knooppunt bewerken", + "frozen_balance": "Bevroren saldo", "invoice_details": "Factuurgegevens", "donation_link_details": "Details van de donatielink", "anonpay_description": "Genereer ${type}. De ontvanger kan ${method} gebruiken met elke ondersteunde cryptocurrency en u ontvangt geld in deze portemonnee", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index b5e0a0bfd..ea20d4da7 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Nie zapomnij podać tagu docelowego podczas wysyłania transakcji XRP do wymiany", "exchange_incorrect_current_wallet_for_xmr" : "Jeśli chcesz wymienić XMR z salda Cake Wallet Monero, najpierw przełącz się na portfel Monero.", - "confirmed" : "Potwierdzony", - "unconfirmed" : "Niepotwierdzony", + "confirmed" : "Potwierdzone saldo", + "unconfirmed" : "Niepotwierdzone saldo", "displayable" : "Wyświetlane", "submit_request" : "Złóż wniosek", @@ -687,6 +687,7 @@ "error_dialog_content": "Ups, wystąpił błąd.\n\nPrześlij raport o awarii do naszego zespołu wsparcia, aby ulepszyć aplikację.", "decimal_places_error": "Za dużo miejsc dziesiętnych", "edit_node": "Edytuj węzeł", + "frozen_balance": "Zamrożona równowaga", "invoice_details": "Dane do faktury", "donation_link_details": "Szczegóły linku darowizny", "anonpay_description": "Wygeneruj ${type}. Odbiorca może ${method} z dowolną obsługiwaną kryptowalutą, a Ty otrzymasz środki w tym portfelu.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 633d2e833..9f088fcef 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Não se esqueça de especificar a etiqueta de destino ao enviar a transação XRP para a troca", "exchange_incorrect_current_wallet_for_xmr" : "Se você deseja trocar o XMR de seu saldo da Carteira Monero Cake, troque primeiro para sua carteira Monero.", - "confirmed" : "Confirmada", - "unconfirmed" : "Não confirmado", + "confirmed" : "Saldo Confirmado", + "unconfirmed" : "Saldo não confirmado", "displayable" : "Exibível", "submit_request" : "enviar um pedido", @@ -686,6 +686,7 @@ "error_dialog_content": "Ops, houve algum erro.\n\nPor favor, envie o relatório de falha para nossa equipe de suporte para melhorar o aplicativo.", "decimal_places_error": "Muitas casas decimais", "edit_node": "Editar nó", + "frozen_balance": "Saldo Congelado", "invoice_details": "Detalhes da fatura", "donation_link_details": "Detalhes do link de doação", "anonpay_description": "Gere ${type}. O destinatário pode ${method} com qualquer criptomoeda suportada e você receberá fundos nesta carteira.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index c9230219c..53dc31b21 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Не забудьте указать целевой тег при отправке транзакции XRP для обмена", "exchange_incorrect_current_wallet_for_xmr" : "Если вы хотите обменять XMR со своего баланса Monero в Cake Wallet, сначала переключитесь на свой кошелек Monero.", - "confirmed" : "Подтверждено", - "unconfirmed" : "Неподтвержденный", + "confirmed" : "Подтвержденный баланс", + "unconfirmed" : "Неподтвержденный баланс", "displayable" : "Отображаемый", "submit_request" : "отправить запрос", @@ -687,6 +687,7 @@ "error_dialog_content": "Ой, у нас какая-то ошибка.\n\nПожалуйста, отправьте отчет о сбое в нашу службу поддержки, чтобы сделать приложение лучше.", "decimal_places_error": "Слишком много десятичных знаков", "edit_node": "Редактировать узел", + "frozen_balance": "Замороженный баланс", "invoice_details": "Детали счета", "donation_link_details": "Информация о ссылке для пожертвований", "anonpay_description": "Создайте ${type}. Получатель может использовать ${method} с любой поддерживаемой криптовалютой, и вы получите средства на этот кошелек.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 1233ecba4..ea7b2afde 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -477,8 +477,8 @@ "xrp_extra_info": "โปรดอย่าลืมระบุ Destination Tag ในขณะที่ส่งธุรกรรม XRP สำหรับการแลกเปลี่ยน", "exchange_incorrect_current_wallet_for_xmr" : "หากคุณต้องการแลกเปลี่ยน XMR จากยอดคงเหลือ Monero ใน Cake Wallet ของคุณ กรุณาเปลี่ยนเป็นกระเป๋า Monero ก่อน", - "confirmed" : "ได้รับการยืนยัน", - "unconfirmed" : "ยังไม่ได้รับการยืนยัน", + "confirmed" : "ยอดคงเหลือที่ยืนยันแล้ว", + "unconfirmed" : "ยอดคงเหลือที่ไม่ได้รับการยืนยัน", "displayable" : "สามารถแสดงได้", "submit_request" : "ส่งคำขอ", @@ -685,6 +685,7 @@ "error_dialog_content": "อ๊ะ เราพบข้อผิดพลาดบางอย่าง\n\nโปรดส่งรายงานข้อขัดข้องไปยังทีมสนับสนุนของเราเพื่อปรับปรุงแอปพลิเคชันให้ดียิ่งขึ้น", "decimal_places_error": "ทศนิยมมากเกินไป", "edit_node": "แก้ไขโหนด", + "frozen_balance": "ยอดคงเหลือแช่แข็ง", "invoice_details": "รายละเอียดใบแจ้งหนี้", "donation_link_details": "รายละเอียดลิงค์บริจาค", "anonpay_description": "สร้าง ${type} ผู้รับสามารถ ${method} ด้วยสกุลเงินดิจิทัลที่รองรับ และคุณจะได้รับเงินในกระเป๋าสตางค์นี้", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 01ba57fbe..7cb72e57a 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -479,8 +479,8 @@ "xrp_extra_info" : "Lütfen takas için XRP işlemi gönderirken Hedef Etiketi (Destination Tag) belirtmeyi unutmayın", "exchange_incorrect_current_wallet_for_xmr" : "Cake Wallet'daki Monero bakiyenizi kullanarak takas yapmak istiyorsan, lütfen önce Monero cüzdanına geç.", - "confirmed" : "Onaylı", - "unconfirmed" : "Onaylanmamış", + "confirmed" : "Onaylanmış Bakiye", + "unconfirmed" : "Onaylanmamış Bakiye", "displayable" : "Gösterilebilir", "submit_request" : "talep gönder", @@ -687,6 +687,7 @@ "error_dialog_content": "Hay aksi, bir hatamız var.\n\nUygulamayı daha iyi hale getirmek için lütfen kilitlenme raporunu destek ekibimize gönderin.", "decimal_places_error": "Çok fazla ondalık basamak", "edit_node": "Düğümü Düzenle", + "frozen_balance": "Dondurulmuş Bakiye", "invoice_details": "fatura detayları", "donation_link_details": "Bağış bağlantısı ayrıntıları", "anonpay_description": "${type} oluşturun. Alıcı, desteklenen herhangi bir kripto para birimi ile ${method} yapabilir ve bu cüzdanda para alırsınız.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 9d8010eca..ac3d32dfd 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -478,8 +478,8 @@ "xrp_extra_info" : "Будь ласка, не забудьте вказати тег призначення під час надсилання XRP-транзакції для обміну", "exchange_incorrect_current_wallet_for_xmr" : "Якщо ви хочете обміняти XMR із вашого балансу Cake Wallet Monero, спочатку перейдіть на свій гаманець Monero.", - "confirmed" : "Підтверджено", - "unconfirmed" : "Непідтверджений", + "confirmed" : "Підтверджений баланс", + "unconfirmed" : "Непідтверджений баланс", "displayable" : "Відображуваний", "submit_request" : "надіслати запит", @@ -686,6 +686,7 @@ "error_dialog_content": "На жаль, ми отримали помилку.\n\nБудь ласка, надішліть звіт про збій нашій команді підтримки, щоб покращити додаток.", "decimal_places_error": "Забагато знаків після коми", "edit_node": "Редагувати вузол", + "frozen_balance": "Заморожений баланс", "invoice_details": "Реквізити рахунку-фактури", "donation_link_details": "Деталі посилання для пожертв", "anonpay_description": "Згенерувати ${type}. Одержувач може ${method} будь-якою підтримуваною криптовалютою, і ви отримаєте кошти на цей гаманець.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 15ca6833b..2e88a0bd6 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -481,8 +481,8 @@ "xrp_extra_info" : "ایکسچینج کے لیے XRP ٹرانزیکشن بھیجتے وقت ڈیسٹینیشن ٹیگ بتانا نہ بھولیں۔", "exchange_incorrect_current_wallet_for_xmr" : "اگر آپ اپنے Cake والیٹ Monero بیلنس سے XMR کا تبادلہ کرنا چاہتے ہیں، تو براہ کرم پہلے اپنے Monero والیٹ پر جائیں۔", - "confirmed" : "تصدیق شدہ", - "unconfirmed" : "غیر تصدیق شدہ", + "confirmed" : "تصدیق شدہ بیلنس", + "unconfirmed" : "غیر تصدیق شدہ بیلنس", "displayable" : "قابل نمائش", "submit_request" : "درخواست بھیج دو", @@ -688,6 +688,7 @@ "error_dialog_content" : "افوہ، ہمیں کچھ خرابی ملی۔\n\nایپلی کیشن کو بہتر بنانے کے لیے براہ کرم کریش رپورٹ ہماری سپورٹ ٹیم کو بھیجیں۔", "decimal_places_error": "بہت زیادہ اعشاریہ جگہیں۔", "edit_node": "نوڈ میں ترمیم کریں۔", + "frozen_balance": "منجمد بیلنس", "invoice_details": "رسید کی تفصیلات", "donation_link_details": "عطیہ کے لنک کی تفصیلات", "anonpay_description": "${type} بنائیں۔ وصول کنندہ کسی بھی تعاون یافتہ کرپٹو کرنسی کے ساتھ ${method} کرسکتا ہے، اور آپ کو اس بٹوے میں فنڈز موصول ہوں گے۔", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d906ebf5b..ea1804b58 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -478,8 +478,8 @@ "xrp_extra_info" : "发送用于交换的XRP交易时,请不要忘记指定目标Tag", "exchange_incorrect_current_wallet_for_xmr" : "如果要从Cake Wallet Monero余额中兑换XMR,请先切换到Monero钱包。", - "confirmed" : "已确认", - "unconfirmed" : "未经证实", + "confirmed" : "确认余额", + "unconfirmed" : "未确认余额", "displayable" : "可显示", "submit_request" : "提交请求", @@ -686,6 +686,7 @@ "error_dialog_content": "糟糕,我们遇到了一些错误。\n\n请将崩溃报告发送给我们的支持团队,以改进应用程序。", "decimal_places_error": "小数位太多", "edit_node": "编辑节点", + "frozen_balance": "冻结余额", "invoice_details": "发票明细", "donation_link_details": "捐赠链接详情", "anonpay_description": "生成 ${type}。收款人可以使用任何受支持的加密货币 ${method},您将在此钱包中收到资金。", From 8f16af4748446c4f0ae83b09f1266740ccd64b3e Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:14:11 +0100 Subject: [PATCH 20/28] CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address (#880) * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address * Delete widget_test.dart * fix: Revert unneeded changes made * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address * fix: Requested changes * Update * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address * fix: Filter address in address book * Update local branch * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address * CW-344-Exchanging-TO-the-current-wallet-currency-should-allow-changing-the-address --- lib/src/screens/contact/contact_list_page.dart | 1 + lib/src/screens/exchange/widgets/exchange_card.dart | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index f8ccf5807..96887ff5f 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -120,6 +120,7 @@ class ContactListPage extends BasePage { if (isCopied) { await Clipboard.setData(ClipboardData(text: contact.address)); await showBar<void>(context, S.of(context).copied_to_clipboard); + } }, child: Container( diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 0ba112215..ae92fa0c1 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -406,7 +406,6 @@ class ExchangeCardState extends State<ExchangeCard> { order: NumericFocusOrder(3), child: BaseTextFormField( controller: addressController, - readOnly: true, borderColor: Colors.transparent, suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), @@ -433,7 +432,10 @@ class ExchangeCardState extends State<ExchangeCard> { onTap: () async { final contact = await Navigator.of(context) - .pushNamed(Routes.pickerAddressBook); + .pushNamed( + Routes.pickerAddressBook, + arguments: widget.initialCurrency, + ); if (contact is ContactBase && contact.address != null) { From ab20312e61a0fca0335cbba6b9f82bc2bf8d0915 Mon Sep 17 00:00:00 2001 From: Justin Ehrenhofer <justin.ehrenhofer@gmail.com> Date: Thu, 20 Apr 2023 13:55:44 -0500 Subject: [PATCH 21/28] Add btcln; add 2 assets to SideShift (#890) * Add btcln; add 2 assets to SideShift * fix regex line * change ordering --- cw_core/lib/crypto_currency.dart | 3 +++ lib/core/address_validator.dart | 4 ++++ lib/exchange/sideshift/sideshift_exchange_provider.dart | 4 ++-- lib/exchange/trocador/trocador_exchange_provider.dart | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 3d076f6e9..da9b7f9e9 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -66,6 +66,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen CryptoCurrency.scrt, CryptoCurrency.uni, CryptoCurrency.stx, + CryptoCurrency.btcln, ]; static const havenCurrencies = [ @@ -150,6 +151,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png'); static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png'); + static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png'); + static final Map<int, CryptoCurrency> _rawCurrencyMap = [...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) { diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 22ad6a9d8..0bed0611c 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -85,6 +85,8 @@ class AddressValidator extends TextValidator { return 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + case CryptoCurrency.btcln: + return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: return '[0-9a-zA-Z]'; } @@ -194,6 +196,8 @@ class AddressValidator extends TextValidator { return [45]; case CryptoCurrency.near: return [64]; + case CryptoCurrency.btcln: + return null; default: return []; } diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index 2fc593988..a5b0c990f 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -29,9 +29,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { CryptoCurrency.dcr, CryptoCurrency.kmd, CryptoCurrency.mkr, - CryptoCurrency.near, CryptoCurrency.oxt, - CryptoCurrency.paxg, CryptoCurrency.pivx, CryptoCurrency.rune, CryptoCurrency.rvn, @@ -300,6 +298,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { return 'usdcsol'; case CryptoCurrency.maticpoly: return 'polygon'; + case CryptoCurrency.btcln: + return 'ln'; default: return currency.title.toLowerCase(); } diff --git a/lib/exchange/trocador/trocador_exchange_provider.dart b/lib/exchange/trocador/trocador_exchange_provider.dart index 39b4638f4..fb6109bdf 100644 --- a/lib/exchange/trocador/trocador_exchange_provider.dart +++ b/lib/exchange/trocador/trocador_exchange_provider.dart @@ -285,6 +285,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { return 'ERC20'; case 'TRX': return 'TRC20'; + case 'LN': + return 'Lightning'; default: return tag.toLowerCase(); } From 3fc927f742c1217cbfdffcb6306fedcf173050c7 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Thu, 20 Apr 2023 23:20:37 +0300 Subject: [PATCH 22/28] CW-289-Fix bitcoin pending send receive transactions not showing until confirmed (#878) * Fix error in rendering pending transactions in different device * Show pending status * Fix broken subscription stream --- cw_bitcoin/lib/electrum.dart | 10 ++++++---- cw_bitcoin/lib/electrum_transaction_info.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index d50f280f6..81f2da161 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -410,7 +410,7 @@ class ElectrumClient { switch (method) { case 'blockchain.scripthash.subscribe': final params = request['params'] as List<dynamic>; - final scripthash = params.first as String; + final scripthash = params.first as String?; final id = 'blockchain.scripthash.subscribe:$scripthash'; _tasks[id]?.subject?.add(params.last); @@ -430,15 +430,17 @@ class ElectrumClient { void _handleResponse(Map<String, dynamic> response) { final method = response['method']; - final id = response['id'] as String; + final id = response['id'] as String?; final result = response['result']; if (method is String) { _methodHandler(method: method, request: response); return; } - - _finish(id, result); + + if (id != null){ + _finish(id, result); + } } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 8d6ef0fea..b034c06b1 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -136,7 +136,7 @@ class ElectrumTransactionInfo extends TransactionInfo { return ElectrumTransactionInfo(type, id: bundle.originalTransaction.getId(), height: height, - isPending: false, + isPending: bundle.confirmations == 0, fee: fee, direction: direction, amount: amount, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4984ad28e..eac05378f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -537,8 +537,8 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, final transactionHex = verboseTransaction['hex'] as String; final original = bitcoin.Transaction.fromHex(transactionHex); final ins = <bitcoin.Transaction>[]; - final time = verboseTransaction['time'] as int; - final confirmations = verboseTransaction['confirmations'] as int ?? 0; + final time = verboseTransaction['time'] as int?; + final confirmations = verboseTransaction['confirmations'] as int? ?? 0; for (final vin in original.ins) { final id = HEX.encode(vin.hash!.reversed.toList()); From 8ffac75e8c939c9dffc54c5b4744b0571a146920 Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Fri, 21 Apr 2023 18:21:31 +0300 Subject: [PATCH 23/28] CW-59-New-update-highlight-popup (#863) * add update pop up * add release notes for monero com * PR comments fixes * Pr coments fixes * minor fixes * update from main * [skip ci] remove unrelated mac os files * add check isNewInstall * update pop-up UI * fix size * Add update popup to desktop dashboard page [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> --- assets/text/Monerocom_Release_Notes.txt | 13 ++ assets/text/Release_Notes.txt | 13 ++ lib/di.dart | 3 +- lib/entities/default_settings_migration.dart | 7 + lib/entities/preferences_key.dart | 4 +- lib/main.dart | 3 +- lib/src/screens/dashboard/dashboard_page.dart | 29 +++- .../dashboard/desktop_dashboard_page.dart | 23 +++ .../dashboard/widgets/filter_widget.dart | 3 +- .../monero_account_list_page.dart | 5 +- .../release_notes/release_notes_screen.dart | 141 ++++++++++++++++++ .../widgets/seed_language_picker.dart | 12 +- lib/src/widgets/alert_close_button.dart | 28 ++-- lib/utils/version_comparator.dart | 13 ++ .../dashboard/dashboard_view_model.dart | 3 +- 15 files changed, 263 insertions(+), 37 deletions(-) create mode 100644 assets/text/Monerocom_Release_Notes.txt create mode 100644 assets/text/Release_Notes.txt create mode 100644 lib/src/screens/release_notes/release_notes_screen.dart create mode 100644 lib/utils/version_comparator.dart diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt new file mode 100644 index 000000000..6a091fc90 --- /dev/null +++ b/assets/text/Monerocom_Release_Notes.txt @@ -0,0 +1,13 @@ +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges +WWEE(enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronization function +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronization function +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronizatio \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt new file mode 100644 index 000000000..6a091fc90 --- /dev/null +++ b/assets/text/Release_Notes.txt @@ -0,0 +1,13 @@ +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges +WWEE(enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronization function +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronization function +Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Changed algorithm for choosing of change address for BTC and LTC electrum wallets +Keep screen awake while the synchronizatio \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 584bc8b8e..d1b4bda42 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -208,7 +208,7 @@ Future setup( required Box<TransactionDescription> transactionDescriptionBox, required Box<Order> ordersSource, Box<UnspentCoinsInfo>? unspentCoinsInfoSource, - required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource + required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource, }) async { _walletInfoSource = walletInfoSource; _nodeSource = nodeSource; @@ -396,6 +396,7 @@ Future setup( dashboardViewModel: getIt.get<DashboardViewModel>(), addressListViewModel: getIt.get<WalletAddressListViewModel>(), )); + getIt.registerFactory<DesktopSidebarWrapper>(() { final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); return DesktopSidebarWrapper( diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index a1a35125b..77298c2b5 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -42,6 +42,13 @@ Future defaultSettingsMigration( // check current nodes for nullability regardless of the version await checkCurrentNodes(nodes, sharedPreferences); + final isNewInstall = sharedPreferences + .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion) == null; + + await sharedPreferences.setBool( + PreferencesKey.isNewInstall, isNewInstall); + + final currentVersion = sharedPreferences .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion) ?? 0; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index f5741a98b..90b57668d 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -41,6 +41,8 @@ class PreferencesKey { static const exchangeProvidersSelection = 'exchange-providers-selection'; static const clearnetDonationLink = 'clearnet_donation_link'; - static const onionDonationLink = 'onion_donation_link'; + static const onionDonationLink = 'onion_donation_link'; + static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; + static const isNewInstall = 'is_new_install'; } diff --git a/lib/main.dart b/lib/main.dart index 8cc2629c9..53ec3fb65 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -187,8 +187,7 @@ Future<void> initialSetup( transactionDescriptionBox: transactionDescriptions, ordersSource: ordersSource, anonpayInvoiceInfoSource: anonpayInvoiceInfo, - unspentCoinsInfoSource: unspentCoinsInfoSource, - ); + unspentCoinsInfoSource: unspentCoinsInfoSource); await bootstrap(navigatorKey); monero?.onStartup(); } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index fbd976aa8..a76b93fc0 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; +import 'package:cake_wallet/utils/version_comparator.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -22,8 +24,12 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart'; class DashboardPage extends StatelessWidget { DashboardPage({ @@ -117,7 +123,7 @@ class _DashboardPageView extends BasePage { @override Widget body(BuildContext context) { final controller = PageController(initialPage: initialPage); - + reaction((_) => dashboardViewModel.shouldShowMarketPlaceInDashboard, (bool value) { if (!dashboardViewModel.shouldShowMarketPlaceInDashboard) { controller.jumpToPage(0); @@ -131,7 +137,7 @@ class _DashboardPageView extends BasePage { } else { controller.jumpToPage(0); } - }); + }); _setEffects(context); return SafeArea( @@ -266,6 +272,25 @@ class _DashboardPageView extends BasePage { } }); + final sharedPrefs = await SharedPreferences.getInstance(); + final currentAppVersion = + VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion); + final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion); + final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall); + + if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) { + await Future<void>.delayed(Duration(seconds: 1)); + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return ReleaseNotesScreen( + title: 'Version ${dashboardViewModel.settingsStore.appVersion}'); + }); + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } else if (isNewInstall!) { + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } + var needToPresentYat = false; var isInactive = false; diff --git a/lib/src/screens/dashboard/desktop_dashboard_page.dart b/lib/src/screens/dashboard/desktop_dashboard_page.dart index 64f8a9aac..df74a3f6f 100644 --- a/lib/src/screens/dashboard/desktop_dashboard_page.dart +++ b/lib/src/screens/dashboard/desktop_dashboard_page.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/utils/version_comparator.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; @@ -11,6 +14,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_v import 'package:mobx/mobx.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/router.dart' as Router; +import 'package:shared_preferences/shared_preferences.dart'; class DesktopDashboardPage extends StatelessWidget { DesktopDashboardPage({ @@ -107,5 +111,24 @@ class DesktopDashboardPage extends StatelessWidget { needToPresentYat = true; }); + + final sharedPrefs = await SharedPreferences.getInstance(); + final currentAppVersion = + VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion); + final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion); + final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall); + + if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) { + await Future<void>.delayed(Duration(seconds: 1)); + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return ReleaseNotesScreen( + title: 'Version ${dashboardViewModel.settingsStore.appVersion}'); + }); + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } else if (isNewInstall!) { + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } } } diff --git a/lib/src/screens/dashboard/widgets/filter_widget.dart b/lib/src/screens/dashboard/widgets/filter_widget.dart index 17df0bc5e..9b8c87ea3 100644 --- a/lib/src/screens/dashboard/widgets/filter_widget.dart +++ b/lib/src/screens/dashboard/widgets/filter_widget.dart @@ -16,7 +16,6 @@ class FilterWidget extends StatelessWidget { FilterWidget({required this.dashboardViewModel}); final DashboardViewModel dashboardViewModel; - final closeIcon = Image.asset('assets/images/close.png', color: Palette.darkBlueCraiola); @override Widget build(BuildContext context) { @@ -101,7 +100,7 @@ class FilterWidget extends StatelessWidget { ), ], ), - AlertCloseButton(image: closeIcon) + AlertCloseButton() ], ), ); diff --git a/lib/src/screens/monero_accounts/monero_account_list_page.dart b/lib/src/screens/monero_accounts/monero_account_list_page.dart index 7fe15948f..145a2d8a4 100644 --- a/lib/src/screens/monero_accounts/monero_account_list_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_list_page.dart @@ -27,9 +27,6 @@ class MoneroAccountListPage extends StatelessWidget { } final MoneroAccountListViewModel accountListViewModel; - final closeIcon = Image.asset('assets/images/close.png', - color: Palette.darkBlueCraiola, - ); ScrollController controller; double backgroundHeight; @@ -163,7 +160,7 @@ class MoneroAccountListPage extends StatelessWidget { ) ], ), - AlertCloseButton(image: closeIcon) + AlertCloseButton() ], ), ); diff --git a/lib/src/screens/release_notes/release_notes_screen.dart b/lib/src/screens/release_notes/release_notes_screen.dart new file mode 100644 index 000000000..f8b3730fb --- /dev/null +++ b/lib/src/screens/release_notes/release_notes_screen.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ReleaseNotesScreen extends StatelessWidget { + const ReleaseNotesScreen({ + required this.title, + }); + + final String title; + + Future<List<String>> _loadStrings() async { + String notesContent = await rootBundle.loadString( + isMoneroOnly ? 'assets/text/Monerocom_Release_Notes.txt' : 'assets/text/Release_Notes.txt'); + return LineSplitter().convert(notesContent); + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + AlertBackground( + child: AlertDialog( + insetPadding: EdgeInsets.only(left: 16, right: 16, bottom: 48), + elevation: 0.0, + contentPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30))), + content: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + gradient: LinearGradient(colors: [ + Theme.of(context).colorScheme.secondary, + Theme.of(context).scaffoldBackgroundColor, + ], begin: Alignment.centerLeft, end: Alignment.centerRight)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + alignment: Alignment.bottomCenter, + child: DefaultTextStyle( + style: TextStyle( + decoration: TextDecoration.none, + fontSize: 24.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + ), + child: Text(title), + ), + ), + ), + ), + SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 48, bottom: 16), + child: Container( + width: double.maxFinite, + child: Column( + children: <Widget>[ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: _getNotesWidget(), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + AlertCloseButton( + bottom: 30, + ) + ], + ); + } + + Widget _getNotesWidget() { + return FutureBuilder<List<String>>( + future: _loadStrings(), + builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) { + if (snapshot.hasData) { + return ListView.builder( + shrinkWrap: true, + itemCount: snapshot.data!.length, + itemBuilder: (BuildContext context, int index) { + return _getNoteItemWidget(snapshot.data![index], context); + }, + ); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Center(child: CircularProgressIndicator()); + } + }, + ); + } + + Widget _getNoteItemWidget(String myString, BuildContext context) { + return Column( + children: [ + DefaultTextStyle( + style: TextStyle( + decoration: TextDecoration.none, + fontSize: 16.0, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text('•'), + ), + Expanded( + child: Text(myString), + ), + ], + )), + SizedBox( + height: 16.0, + ) + ], + ); + } +} diff --git a/lib/src/screens/seed_language/widgets/seed_language_picker.dart b/lib/src/screens/seed_language/widgets/seed_language_picker.dart index 0e1e63f57..0aa22088f 100644 --- a/lib/src/screens/seed_language/widgets/seed_language_picker.dart +++ b/lib/src/screens/seed_language/widgets/seed_language_picker.dart @@ -47,23 +47,19 @@ const List<String> seedLanguages = [ enum Places { topLeft, topRight, bottomLeft, bottomRight, inside } class SeedLanguagePicker extends StatefulWidget { - SeedLanguagePicker( - {Key? key, - this.selected = defaultSeedLanguage, - required this.onItemSelected}) + SeedLanguagePicker({Key? key, this.selected = defaultSeedLanguage, required this.onItemSelected}) : super(key: key); final String selected; final Function(String) onItemSelected; @override - SeedLanguagePickerState createState() => SeedLanguagePickerState( - selected: selected, onItemSelected: onItemSelected); + SeedLanguagePickerState createState() => + SeedLanguagePickerState(selected: selected, onItemSelected: onItemSelected); } class SeedLanguagePickerState extends State<SeedLanguagePicker> { - SeedLanguagePickerState( - {required this.selected, required this.onItemSelected}); + SeedLanguagePickerState({required this.selected, required this.onItemSelected}); final String selected; final Function(String) onItemSelected; diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index 1aa8277f3..a3657190a 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -15,20 +15,18 @@ class AlertCloseButton extends StatelessWidget { @override Widget build(BuildContext context) { return Positioned( - bottom: bottom ?? 60, - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - height: 42, - width: 42, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle - ), - child: Center( - child: image ?? closeButton, - ), + bottom: bottom ?? 60, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Center( + child: image ?? closeButton, ), - )); + ), + ), + ); } -} \ No newline at end of file +} diff --git a/lib/utils/version_comparator.dart b/lib/utils/version_comparator.dart new file mode 100644 index 000000000..e0864568a --- /dev/null +++ b/lib/utils/version_comparator.dart @@ -0,0 +1,13 @@ +class VersionComparator { + static bool isVersion1Greater({required String v1, required String v2}) { + int v1Number = getExtendedVersionNumber(v1); + int v2Number = getExtendedVersionNumber(v2); + return v1Number > v2Number; + } + + static int getExtendedVersionNumber(String version) { + List<String> stringVersionCells = version.split('.'); + List<int> intVersionCells = stringVersionCells.map((i) => int.parse(i)).toList(); + return intVersionCells[0] * 100000 + intVersionCells[1] * 1000 + intVersionCells[2]; + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 6cfbe1455..c28603a51 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -43,8 +43,7 @@ abstract class DashboardViewModelBase with Store { required this.settingsStore, required this.yatStore, required this.ordersStore, - required this.anonpayTransactionsStore, - }) + required this.anonpayTransactionsStore}) : isOutdatedElectrumWallet = false, hasSellAction = false, isEnabledSellAction = false, From f2b8dd21a1acd4e5eedfe314782780b21d022217 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:03:42 +0300 Subject: [PATCH 24/28] CW-240 Receive fiat currency amount and receive animations (#877) * Redesign receive amount field * Fix issues with animations * Fix issues with animations * Fix max fraction digit to 8 * add another 0 * Update amount when currency is changed --------- Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com> Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> --- lib/core/amount_validator.dart | 5 +- lib/di.dart | 9 +- lib/entities/qr_view_data.dart | 11 + lib/router.dart | 9 +- .../dashboard/widgets/address_page.dart | 75 +++-- lib/src/screens/exchange/exchange_page.dart | 8 +- .../screens/receive/anonpay_receive_page.dart | 8 +- .../screens/receive/fullscreen_qr_page.dart | 10 +- lib/src/screens/receive/receive_page.dart | 299 +++++++++--------- .../receive/widgets/currency_input_field.dart | 120 +++++++ .../screens/receive/widgets/qr_widget.dart | 158 ++++----- .../screens/wallet_keys/wallet_keys_page.dart | 5 +- .../wallet_address_list_view_model.dart | 127 ++++---- 13 files changed, 504 insertions(+), 340 deletions(-) create mode 100644 lib/entities/qr_view_data.dart create mode 100644 lib/src/screens/receive/widgets/currency_input_field.dart diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index acd0ab135..fb5214d54 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; class AmountValidator extends TextValidator { AmountValidator({ @@ -57,7 +58,7 @@ class SymbolsAmountValidator extends TextValidator { } class DecimalAmountValidator extends TextValidator { - DecimalAmountValidator({required CryptoCurrency currency, required bool isAutovalidate }) + DecimalAmountValidator({required Currency currency, required bool isAutovalidate }) : super( errorMessage: S.current.decimal_places_error, pattern: _pattern(currency), @@ -65,7 +66,7 @@ class DecimalAmountValidator extends TextValidator { minLength: 0, maxLength: 0); - static String _pattern(CryptoCurrency currency) { + static String _pattern(Currency currency) { switch (currency) { case CryptoCurrency.xmr: return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$'; diff --git a/lib/di.dart b/lib/di.dart index d1b4bda42..78a9b7802 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -183,6 +183,7 @@ import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; final getIt = GetIt.instance; @@ -321,7 +322,9 @@ Future setup( getIt.registerFactory<WalletAddressListViewModel>(() => WalletAddressListViewModel( - appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>())); + appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>(), + fiatConversionStore: getIt.get<FiatConversionStore>() + )); getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get<AppStore>(), @@ -815,8 +818,8 @@ Future setup( getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(), walletType: getIt.get<AppStore>().wallet!.type)); - getIt.registerFactoryParam<FullscreenQRPage, String, int?>( - (String qrData, int? version) => FullscreenQRPage(qrData: qrData, version: version,)); + getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>( + (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); getIt.registerFactory(() => IoniaApi()); diff --git a/lib/entities/qr_view_data.dart b/lib/entities/qr_view_data.dart new file mode 100644 index 000000000..a975d137b --- /dev/null +++ b/lib/entities/qr_view_data.dart @@ -0,0 +1,11 @@ +class QrViewData { + final int? version; + final String? heroTag; + final String data; + + QrViewData({ + this.version, + this.heroTag, + required this.data, + }); +} \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index aebee0942..103a0889e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; @@ -242,7 +243,7 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.receive: return CupertinoPageRoute<void>( - fullscreenDialog: true, builder: (_) => getIt.get<ReceivePage>()); + builder: (_) => getIt.get<ReceivePage>()); case Routes.addressPage: return CupertinoPageRoute<void>( @@ -451,14 +452,10 @@ Route<dynamic> createRoute(RouteSettings settings) { param1: args)); case Routes.fullscreenQR: - final args = settings.arguments as Map<String, dynamic>; - return MaterialPageRoute<void>( builder: (_) => getIt.get<FullscreenQRPage>( - param1: args['qrData'] as String, - param2: args['version'] as int?, - + param1: settings.arguments as QrViewData, )); case Routes.ioniaWelcomePage: diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index ebfabcd02..cdaa22673 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -26,11 +26,23 @@ class AddressPage extends BasePage { required this.addressListViewModel, required this.dashboardViewModel, required this.receiveOptionViewModel, - }) : _cryptoAmountFocus = FocusNode(); + }) : _cryptoAmountFocus = FocusNode(), + _formKey = GlobalKey<FormState>(), + _amountController = TextEditingController(){ + _amountController.addListener(() { + if (_formKey.currentState!.validate()) { + addressListViewModel.changeAmount( + _amountController.text, + ); + } + }); + } final WalletAddressListViewModel addressListViewModel; final DashboardViewModel dashboardViewModel; final ReceiveOptionViewModel receiveOptionViewModel; + final TextEditingController _amountController; + final GlobalKey<FormState> _formKey; final FocusNode _cryptoAmountFocus; @@ -69,28 +81,27 @@ class AddressPage extends BasePage { @override Widget? trailing(BuildContext context) { - final shareImage = Image.asset('assets/images/share.png', - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); - - return !addressListViewModel.hasAddressList - ? Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.address.address, - context: context, - ); - }, - icon: shareImage, - ), - ) - : null; + return Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.uri.toString(), + context: context, + ); + }, + icon: Icon( + Icons.share, + size: 20, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + ), + ), + ); } @override @@ -137,16 +148,18 @@ class AddressPage extends BasePage { ) ]), child: Container( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + padding: EdgeInsets.fromLTRB(24, 0, 24, 32), child: Column( children: <Widget>[ - Expanded( - child: Observer(builder: (_) => QRWidget( - addressListViewModel: addressListViewModel, - amountTextFieldFocusNode: _cryptoAmountFocus, - isAmountFieldShow: !addressListViewModel.hasAccounts, - isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light)) - ), + Expanded( + child: Observer( + builder: (_) => QRWidget( + formKey: _formKey, + addressListViewModel: addressListViewModel, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: dashboardViewModel.settingsStore.currentTheme.type == + ThemeType.light))), Observer(builder: (_) { return addressListViewModel.hasAddressList ? GestureDetector( diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 310e40cd8..a434ed13b 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -115,10 +115,6 @@ class ExchangePage extends BasePage { WidgetsBinding.instance .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); - if (exchangeViewModel.isLowFee) { - _showFeeAlert(context); - } - return KeyboardActions( disableScroll: true, config: KeyboardActionsConfig( @@ -319,6 +315,10 @@ class ExchangePage extends BasePage { return; } + if (exchangeViewModel.isLowFee) { + _showFeeAlert(context); + } + final depositAddressController = depositKey.currentState!.addressController; final depositAmountController = depositKey.currentState!.amountController; final receiveAddressController = receiveKey.currentState!.addressController; diff --git a/lib/src/screens/receive/anonpay_receive_page.dart b/lib/src/screens/receive/anonpay_receive_page.dart index 27b5d41a3..1ee947d49 100644 --- a/lib/src/screens/receive/anonpay_receive_page.dart +++ b/lib/src/screens/receive/anonpay_receive_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -133,10 +134,9 @@ class AnonPayReceivePage extends BasePage { await Navigator.pushNamed( context, Routes.fullscreenQR, - arguments: { - 'qrData': invoiceInfo.clearnetUrl, - 'version': qr.QrVersions.auto, - }, + arguments: QrViewData(data: invoiceInfo.clearnetUrl, + version: qr.QrVersions.auto, + ) ); // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(brightness); diff --git a/lib/src/screens/receive/fullscreen_qr_page.dart b/lib/src/screens/receive/fullscreen_qr_page.dart index 885c548f0..4bde38710 100644 --- a/lib/src/screens/receive/fullscreen_qr_page.dart +++ b/lib/src/screens/receive/fullscreen_qr_page.dart @@ -1,13 +1,13 @@ +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; class FullscreenQRPage extends BasePage { - FullscreenQRPage({required this.qrData, int? this.version}); + FullscreenQRPage({required this.qrViewData}); - final String qrData; - final int? version; + final QrViewData qrViewData; @override Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @@ -63,7 +63,7 @@ class FullscreenQRPage extends BasePage { return Padding( padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width * 0.05), child: Hero( - tag: Key(qrData), + tag: Key(qrViewData.heroTag ?? qrViewData.data), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -71,7 +71,7 @@ class FullscreenQRPage extends BasePage { padding: EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(width: 3, color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)), - child: QrImage(data: qrData, version: version), + child: QrImage(data: qrViewData.data, version: qrViewData.version), ), ), ), diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 53bda35d8..00a157d97 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -21,16 +21,28 @@ import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; class ReceivePage extends BasePage { - ReceivePage({required this.addressListViewModel}) : _cryptoAmountFocus = FocusNode(); + ReceivePage({required this.addressListViewModel}) + : _cryptoAmountFocus = FocusNode(), + _amountController = TextEditingController(), + _formKey = GlobalKey<FormState>() { + _amountController.addListener(() { + if (_formKey.currentState!.validate()) { + addressListViewModel.changeAmount(_amountController.text); + } + }); + } final WalletAddressListViewModel addressListViewModel; + final TextEditingController _amountController; + final GlobalKey<FormState> _formKey; + static const _heroTag = 'receive_page'; @override String get title => S.current.receive; @override - Color get backgroundLightColor => currentTheme.type == ThemeType.bright - ? Colors.transparent : Colors.white; + Color get backgroundLightColor => + currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @override Color get backgroundDarkColor => Colors.transparent; @@ -68,162 +80,153 @@ class ReceivePage extends BasePage { @override Widget trailing(BuildContext context) { - final shareImage = - Image.asset('assets/images/share.png', - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); - return Material( color: Colors.transparent, child: Semantics( label: 'Share', child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.address.address, - context: context, - ); - }, - icon: shareImage + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.uri.toString(), + context: context, + ); + }, + icon: Icon( + Icons.share, + size: 20, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + ), ), - ) - ); + )); } @override Widget body(BuildContext context) { - return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) + return (addressListViewModel.type == WalletType.monero || + addressListViewModel.type == WalletType.haven) ? KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1! - .backgroundColor!, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: SingleChildScrollView( - child: Column( - children: <Widget>[ - Padding( - padding: EdgeInsets.fromLTRB(24, 80, 24, 24), - child: QRWidget( - addressListViewModel: addressListViewModel, - isAmountFieldShow: true, - amountTextFieldFocusNode: _cryptoAmountFocus, - isLight: currentTheme.type == ThemeType.light), + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ]), + child: SingleChildScrollView( + child: Column( + children: <Widget>[ + Padding( + padding: EdgeInsets.fromLTRB(24, 50, 24, 24), + child: QRWidget( + addressListViewModel: addressListViewModel, + formKey: _formKey, + heroTag: _heroTag, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light), + ), + Observer( + builder: (_) => ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const SectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); + + if (item is WalletAccountListHeader) { + cell = HeaderTile( + onTap: () async => await showPopUp<void>( + context: context, + builder: (_) => getIt.get<MoneroAccountListPage>()), + title: S.of(context).accounts, + icon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).textTheme!.headline4!.color!, + )); + } + + if (item is WalletAddressListHeader) { + cell = HeaderTile( + onTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress), + title: S.of(context).addresses, + icon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).textTheme!.headline4!.color!, + )); + } + + if (item is WalletAddressListItem) { + cell = Observer(builder: (_) { + final isCurrent = + item.address == addressListViewModel.address.address; + final backgroundColor = isCurrent + ? Theme.of(context).textTheme!.headline2!.decorationColor! + : Theme.of(context).textTheme!.headline3!.decorationColor!; + final textColor = isCurrent + ? Theme.of(context).textTheme!.headline2!.color! + : Theme.of(context).textTheme!.headline3!.color!; + + return AddressCell.fromItem(item, + isCurrent: isCurrent, + backgroundColor: backgroundColor, + textColor: textColor, + onTap: (_) => addressListViewModel.setAddress(item), + onEdit: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item)); + }); + } + + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30)), + child: cell, + ); + })), + ], ), - Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const SectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); - - if (item is WalletAccountListHeader) { - cell = HeaderTile( - onTap: () async => await showPopUp<void>( - context: context, - builder: (_) => - getIt.get<MoneroAccountListPage>()), - title: S.of(context).accounts, - icon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: - Theme.of(context).textTheme!.headline4!.color!, - )); - } - - if (item is WalletAddressListHeader) { - cell = HeaderTile( - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, - icon: Icon( - Icons.add, - size: 20, - color: - Theme.of(context).textTheme!.headline4!.color!, - )); - } - - if (item is WalletAddressListItem) { - cell = Observer(builder: (_) { - final isCurrent = item.address == - addressListViewModel.address.address; - final backgroundColor = isCurrent - ? Theme.of(context) - .textTheme! - .headline2! - .decorationColor! - : Theme.of(context) - .textTheme! - .headline3! - .decorationColor!; - final textColor = isCurrent - ? Theme.of(context).textTheme!.headline2!.color! - : Theme.of(context).textTheme!.headline3!.color!; - - return AddressCell.fromItem(item, - isCurrent: isCurrent, - backgroundColor: backgroundColor, - textColor: textColor, - onTap: (_) => addressListViewModel.setAddress(item), - onEdit: () => Navigator.of(context).pushNamed( - Routes.newSubaddress, - arguments: item)); - }); - } - - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30)), - child: cell, - ); - })), - ], - ), - )) : Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), - child: Column( - children: [ - Expanded( - flex: 7, - child: QRWidget( - addressListViewModel: addressListViewModel, - isAmountFieldShow: true, - amountTextFieldFocusNode: _cryptoAmountFocus, - isLight: currentTheme.type == ThemeType.light), - ), - Expanded( - flex: 2, - child: SizedBox(), - ), - Text(S.of(context).electrum_address_disclaimer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor!)), - ], - ), - ); + )) + : Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Column( + children: [ + Expanded( + flex: 7, + child: QRWidget( + formKey: _formKey, + heroTag: _heroTag, + addressListViewModel: addressListViewModel, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light), + ), + Expanded( + flex: 2, + child: SizedBox(), + ), + Text(S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!)), + ], + ), + ); } } diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart new file mode 100644 index 000000000..286c6f1cd --- /dev/null +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -0,0 +1,120 @@ +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cw_core/currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CurrencyInputField extends StatelessWidget { + const CurrencyInputField({ + super.key, + required this.onTapPicker, + required this.selectedCurrency, + this.focusNode, + required this.controller, + }); + final Function() onTapPicker; + final Currency selectedCurrency; + final FocusNode? focusNode; + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + final arrowBottomPurple = Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ); + final _width = MediaQuery.of(context).size.width; + + return Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 20), + child: SizedBox( + height: 40, + child: BaseTextFormField( + focusNode: focusNode, + controller: controller, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,8}'))], + hintText: '0.000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline5!.color!, + fontWeight: FontWeight.w600, + ), + borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + left: _width / 4, + ), + child: Container( + padding: EdgeInsets.only(right: 8), + child: InkWell( + onTap: onTapPicker, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Padding( + padding: EdgeInsets.only(right: 5), + child: arrowBottomPurple, + ), + Text( + selectedCurrency.name.toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white, + ), + ), + if (selectedCurrency.tag != null) + Padding( + padding: const EdgeInsets.only(right: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryTextTheme.headline4!.color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + ), + child: Center( + child: Text( + selectedCurrency.tag!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .headline4! + .decorationColor!, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.0), + child: Text( + ':', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, + color: Colors.white, + ), + ), + ), + ]), + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9e68ff0e1..fc58d4cec 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,37 +1,36 @@ +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; class QRWidget extends StatelessWidget { - QRWidget( - {required this.addressListViewModel, - required this.isLight, - this.qrVersion, - this.isAmountFieldShow = false, - this.amountTextFieldFocusNode}) - : amountController = TextEditingController(), - _formKey = GlobalKey<FormState>() { - amountController.addListener(() => addressListViewModel?.amount = - _formKey.currentState!.validate() ? amountController.text : ''); - } + QRWidget({ + required this.addressListViewModel, + required this.isLight, + this.qrVersion, + this.heroTag, + required this.amountController, + required this.formKey, + this.amountTextFieldFocusNode, + }); final WalletAddressListViewModel addressListViewModel; - final bool isAmountFieldShow; final TextEditingController amountController; final FocusNode? amountTextFieldFocusNode; - final GlobalKey<FormState> _formKey; + final GlobalKey<FormState> formKey; final bool isLight; final int? qrVersion; + final String? heroTag; @override Widget build(BuildContext context) { @@ -40,7 +39,7 @@ class QRWidget extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Column( @@ -63,18 +62,18 @@ class QRWidget extends StatelessWidget { flex: 5, child: GestureDetector( onTap: () { - changeBrightnessForRoute(() async { - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: { - 'qrData': addressListViewModel.uri.toString(), - }, - ); - }); + changeBrightnessForRoute( + () async { + await Navigator.pushNamed(context, Routes.fullscreenQR, + arguments: QrViewData( + data: addressListViewModel.uri.toString(), + heroTag: heroTag, + )); + }, + ); }, child: Hero( - tag: Key(addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressListViewModel.uri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -83,7 +82,8 @@ class QRWidget extends StatelessWidget { decoration: BoxDecoration( border: Border.all( width: 3, - color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + color: + Theme.of(context).accentTextTheme.headline2!.backgroundColor!, ), ), child: QrImage(data: addressListViewModel.uri.toString()), @@ -99,77 +99,77 @@ class QRWidget extends StatelessWidget { ), ], ), - if (isAmountFieldShow) - Padding( + Observer(builder: (_) { + return Padding( padding: EdgeInsets.only(top: 10), child: Row( children: <Widget>[ Expanded( child: Form( - key: _formKey, - child: BaseTextFormField( + key: formKey, + child: CurrencyInputField( focusNode: amountTextFieldFocusNode, controller: amountController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], - textAlign: TextAlign.center, - hintText: S.of(context).receive_amount, - textColor: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, - borderColor: Theme.of(context).textTheme!.headline5!.decorationColor!, - validator: AmountValidator( - currency: walletTypeToCryptoCurrency(addressListViewModel!.type), - isAutovalidate: true), - // FIX-ME: Check does it equal to autovalidate: true, - autovalidateMode: AutovalidateMode.always, - placeholderTextStyle: TextStyle( - color: Theme.of(context).hoverColor, - fontSize: 18, - fontWeight: FontWeight.w500, - ), + onTapPicker: () => _presentPicker(context), + selectedCurrency: addressListViewModel.selectedCurrency, ), ), ), ], ), - ), - Padding( - padding: EdgeInsets.only(top: 8, bottom: 8), - child: Builder( - builder: (context) => Observer( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel!.address.address)); - showBar<void>(context, S.of(context).copied_to_clipboard); - }, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Expanded( - child: Text( - addressListViewModel!.address.address, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: - Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), - ), + ); + }), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 8), + child: Builder( + builder: (context) => Observer( + builder: (context) => GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + showBar<void>(context, S.of(context).copied_to_clipboard); + }, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Expanded( + child: Text( + addressListViewModel.address.address, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!), ), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - ) - ], - ), + ), + Padding( + padding: EdgeInsets.only(left: 12), + child: copyImage, + ) + ], ), ), ), - ) + ), + ) ], ); } + void _presentPicker(BuildContext context) async { + await showPopUp<void>( + builder: (_) => CurrencyPicker( + selectedAtIndex: addressListViewModel.selectedCurrencyIndex, + items: addressListViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: addressListViewModel.selectCurrency, + ), + context: context, + ); + // update amount if currency changed + addressListViewModel.changeAmount(amountController.text); + } + Future<void> changeBrightnessForRoute(Future<void> Function() navigation) async { // if not mobile, just navigate if (!DeviceInfo.instance.isMobile) { diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 8c378d174..eb34393cf 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; @@ -31,9 +32,7 @@ class WalletKeysPage extends BasePage { await Navigator.pushNamed( context, Routes.fullscreenQR, - arguments: { - 'qrData': (await walletKeysViewModel.url).toString(), - }, + arguments: QrViewData(data: await walletKeysViewModel.url.toString()), ); // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(brightness); diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index b861261a6..a5e3a6ca7 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,5 +1,8 @@ +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cw_core/currency.dart'; +import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/utils/list_item.dart'; @@ -11,37 +14,30 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'dart:async'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase - with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; abstract class PaymentURI { - PaymentURI({ - required this.amount, - required this.address}); + PaymentURI({required this.amount, required this.address}); final String amount; final String address; } class MoneroURI extends PaymentURI { - MoneroURI({ - required String amount, - required String address}) + MoneroURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'monero:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; } @@ -50,16 +46,14 @@ class MoneroURI extends PaymentURI { } class HavenURI extends PaymentURI { - HavenURI({ - required String amount, - required String address}) + HavenURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'haven:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; } @@ -68,16 +62,14 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({ - required String amount, - required String address}) + BitcoinURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'bitcoin:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -86,16 +78,14 @@ class BitcoinURI extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({ - required String amount, - required String address}) + LitecoinURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'litecoin:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -106,24 +96,33 @@ class LitecoinURI extends PaymentURI { abstract class WalletAddressListViewModelBase with Store { WalletAddressListViewModelBase({ required AppStore appStore, - required this.yatStore - }) : _appStore = appStore, - _baseItems = <ListItem>[], - _wallet = appStore.wallet!, - hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, - amount = '' { - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< - Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? - wallet) { - if (wallet == null) { - return; - } - _wallet = wallet; - hasAccounts = _wallet.type == WalletType.monero; - }); + required this.yatStore, + required this.fiatConversionStore, + }) : _appStore = appStore, + _baseItems = <ListItem>[], + _wallet = appStore.wallet!, + selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), + _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), + hasAccounts = + appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, + amount = '' { _init(); } + static const String _cryptoNumberPattern = '0.00000000'; + + final NumberFormat _cryptoNumberFormat; + + final FiatConversionStore fiatConversionStore; + + List<Currency> get currencies => [walletTypeToCryptoCurrency(_wallet.type), ...FiatCurrency.all]; + + @observable + Currency selectedCurrency; + + @computed + int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); + @observable String amount; @@ -156,8 +155,9 @@ abstract class WalletAddressListViewModelBase with Store { } @computed - ObservableList<ListItem> get items => - ObservableList<ListItem>()..addAll(_baseItems)..addAll(addressList); + ObservableList<ListItem> get items => ObservableList<ListItem>() + ..addAll(_baseItems) + ..addAll(addressList); @computed ObservableList<ListItem> get addressList { @@ -166,10 +166,7 @@ abstract class WalletAddressListViewModelBase with Store { if (wallet.type == WalletType.monero) { final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = monero - !.getSubaddressList(wallet) - .subaddresses - .map((subaddress) { + final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -183,10 +180,7 @@ abstract class WalletAddressListViewModelBase with Store { if (wallet.type == WalletType.haven) { final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = haven - !.getSubaddressList(wallet) - .subaddresses - .map((subaddress) { + final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -203,8 +197,7 @@ abstract class WalletAddressListViewModelBase with Store { final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { final isPrimary = addr == primaryAddress; - return WalletAddressListItem( - isPrimary: isPrimary, name: null, address: addr); + return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); }); addressList.addAll(bitcoinAddresses); } @@ -234,8 +227,7 @@ abstract class WalletAddressListViewModelBase with Store { bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven; @observable - WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> - _wallet; + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet; List<ListItem> _baseItems; @@ -243,8 +235,6 @@ abstract class WalletAddressListViewModelBase with Store { final YatStore yatStore; - ReactionDisposer? _onWalletChangeReaction; - @action void setAddress(WalletAddressListItem address) => _wallet.walletAddresses.address = address.address; @@ -258,4 +248,31 @@ abstract class WalletAddressListViewModelBase with Store { _baseItems.add(WalletAddressListHeader()); } + + @action + void selectCurrency(Currency currency) { + selectedCurrency = currency; + } + + @action + void changeAmount(String amount) { + this.amount = amount; + if (selectedCurrency is FiatCurrency) { + _convertAmountToCrypto(); + } + } + + void _convertAmountToCrypto() { + final cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type); + try { + final crypto = + double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!; + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); + if (amount != cryptoAmountTmp) { + amount = cryptoAmountTmp; + } + } catch (e) { + amount = ''; + } + } } From 1eb8d0c698e9a61a8670be876ece665b3bd3a95f Mon Sep 17 00:00:00 2001 From: Serhii <borodenko.sv@gmail.com> Date: Fri, 21 Apr 2023 21:36:47 +0300 Subject: [PATCH 25/28] CW-229 Improved restore options from QR code (#793) * add restoring wallet from qr * add restore mode * add alert for exceptions * add restore from seed * add check for create wallet state * convert sweeping page into stateful * fix parsing url * restoration flow update * update restoring from key mode * update config * fix restor of BTC and LTC wallets * fix pin code issue * wallet Seed/keys uri or code fix * fix key restore credentials * update the restore workflow * update from main * PR coments fixes * update * update * PR fixes --- lib/di.dart | 13 +- lib/entities/parse_address_from_domain.dart | 2 +- lib/router.dart | 20 ++- lib/routes.dart | 1 + .../screens/restore/restore_options_page.dart | 78 +++++++-- .../screens/restore/sweeping_wallet_page.dart | 123 ++++++++++++++ .../screens/restore/wallet_restore_page.dart | 1 + .../screens/wallet_list/wallet_list_page.dart | 2 +- lib/src/screens/welcome/welcome_page.dart | 3 +- .../restore/restore_from_qr_vm.dart | 106 ++++++++++++ lib/view_model/restore/restore_mode.dart | 1 + lib/view_model/restore/restore_wallet.dart | 66 ++++++++ .../restore/wallet_restore_from_qr_code.dart | 159 ++++++++++++++++++ lib/view_model/wallet_creation_vm.dart | 20 ++- lib/view_model/wallet_keys_view_model.dart | 13 +- lib/view_model/wallet_new_vm.dart | 1 + .../wallet_restoration_from_keys_vm.dart | 3 +- .../wallet_restoration_from_seed_vm.dart | 1 + lib/view_model/wallet_restore_view_model.dart | 3 +- res/values/strings_ar.arb | 7 +- res/values/strings_de.arb | 7 +- res/values/strings_en.arb | 7 +- res/values/strings_es.arb | 7 +- res/values/strings_fr.arb | 7 +- res/values/strings_hi.arb | 7 +- res/values/strings_hr.arb | 7 +- res/values/strings_it.arb | 7 +- res/values/strings_ja.arb | 7 +- res/values/strings_ko.arb | 7 +- res/values/strings_my.arb | 7 +- res/values/strings_nl.arb | 7 +- res/values/strings_pl.arb | 7 +- res/values/strings_pt.arb | 7 +- res/values/strings_ru.arb | 7 +- res/values/strings_th.arb | 7 +- res/values/strings_tr.arb | 7 +- res/values/strings_uk.arb | 7 +- res/values/strings_zh.arb | 7 +- 38 files changed, 698 insertions(+), 51 deletions(-) create mode 100644 lib/src/screens/restore/sweeping_wallet_page.dart create mode 100644 lib/view_model/restore/restore_from_qr_vm.dart create mode 100644 lib/view_model/restore/restore_mode.dart create mode 100644 lib/view_model/restore/restore_wallet.dart create mode 100644 lib/view_model/restore/wallet_restore_from_qr_code.dart diff --git a/lib/di.dart b/lib/di.dart index 78a9b7802..1667fd1d2 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -53,6 +53,8 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; @@ -320,6 +322,13 @@ Future setup( type: type, language: language); }); + getIt + .registerFactoryParam<WalletRestorationFromQRVM, WalletType, void>((WalletType type, _) { + return WalletRestorationFromQRVM(getIt.get<AppStore>(), + getIt.get<WalletCreationService>(param1: type), + _walletInfoSource, type); + }); + getIt.registerFactory<WalletAddressListViewModel>(() => WalletAddressListViewModel( appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>(), @@ -743,7 +752,9 @@ Future setup( getIt.registerFactory( () => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>())); - getIt.registerFactory(() => RestoreOptionsPage()); + getIt.registerFactoryParam<RestoreOptionsPage, bool, void>((bool isNewInstall, _) => + RestoreOptionsPage(isNewInstall: isNewInstall)); + getIt.registerFactory( () => RestoreFromBackupViewModel(getIt.get<BackupService>())); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 0aefa5afe..8ac9bb51f 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -33,7 +33,7 @@ class AddressResolver { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern == null) { - throw 'Unexpected token: $type for getAddressFromStringPattern'; + throw Exception('Unexpected token: $type for getAddressFromStringPattern'); } final match = RegExp(addressPattern).firstMatch(raw); diff --git a/lib/router.dart b/lib/router.dart index 103a0889e..5a657a9ca 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/onramper_page.dart'; import 'package:cake_wallet/src/screens/buy/payfura_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; +import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart'; @@ -40,6 +41,8 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -158,8 +161,9 @@ Route<dynamic> createRoute(RouteSettings settings) { param2: false)); case Routes.restoreOptions: + final isNewInstall = settings.arguments as bool; return CupertinoPageRoute<void>( - builder: (_) => getIt.get<RestoreOptionsPage>()); + builder: (_) => getIt.get<RestoreOptionsPage>(param1: isNewInstall)); case Routes.restoreWalletOptions: final type = WalletType.monero; //settings.arguments as WalletType; @@ -189,12 +193,18 @@ Route<dynamic> createRoute(RouteSettings settings) { })); case Routes.restoreWalletOptionsFromWelcome: - return CupertinoPageRoute<void>( + final isNewInstall = settings.arguments as bool; + return isNewInstall ? CupertinoPageRoute<void>( builder: (_) => getIt.get<SetupPinCodePage>( param1: (PinCodeState<PinCodeWidget> context, dynamic _) => Navigator.pushNamed( context.context, Routes.restoreWalletType)), - fullscreenDialog: true); + fullscreenDialog: true) : CupertinoPageRoute<void>( + builder: (_) => getIt.get<NewWalletTypePage>( + param1: (BuildContext context, WalletType type) => + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: type), + param2: false)); case Routes.seed: return MaterialPageRoute<void>( @@ -224,6 +234,10 @@ Route<dynamic> createRoute(RouteSettings settings) { builder: (_) => RestoreWalletFromKeysPage( walletRestorationFromKeysVM: walletRestorationFromKeysVM)); + case Routes.sweepingWalletPage: + return CupertinoPageRoute<void>( + builder: (_) => getIt.get<SweepingWalletPage>()); + case Routes.dashboard: return CupertinoPageRoute<void>( builder: (_) => getIt.get<DashboardPage>()); diff --git a/lib/routes.dart b/lib/routes.dart index 295b55ae0..823febe78 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -84,6 +84,7 @@ class Routes { static const displaySettingsPage = '/display_settings_page'; static const otherSettingsPage = '/other_settings_page'; static const advancedPrivacySettings = '/advanced_privacy_settings'; + static const sweepingWalletPage = '/sweeping_wallet_page'; static const anonPayInvoicePage = '/anon_pay_invoice_page'; static const anonPayReceivePage = '/anon_pay_receive_page'; static const anonPayDetailsPage = '/anon_pay_details_page'; diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index a7eb03778..8025ebd85 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,3 +1,11 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/language_list.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; +import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -7,15 +15,16 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; class RestoreOptionsPage extends BasePage { - RestoreOptionsPage(); - - static const _aspectRatioImage = 2.086; + RestoreOptionsPage({required this.isNewInstall}); @override String get title => S.current.restore_restore_wallet; + + final bool isNewInstall; final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png'); final imageBackup = Image.asset('assets/images/backup.png'); + final qrCode = Image.asset('assets/images/qr_code_icon.png'); @override Widget body(BuildContext context) { @@ -28,24 +37,69 @@ class RestoreOptionsPage extends BasePage { child: Column( children: <Widget>[ RestoreButton( - onPressed: () => - Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome), + onPressed: () => Navigator.pushNamed( + context, Routes.restoreWalletOptionsFromWelcome, + arguments: isNewInstall), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, - description: - S.of(context).restore_description_from_seed_keys), + description: S.of(context).restore_description_from_seed_keys), + if (isNewInstall) + Padding( + padding: EdgeInsets.only(top: 24), + child: RestoreButton( + onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), + image: imageBackup, + title: S.of(context).restore_title_from_backup, + description: S.of(context).restore_description_from_backup), + ), Padding( padding: EdgeInsets.only(top: 24), child: RestoreButton( - onPressed: () => - Navigator.pushNamed(context, Routes.restoreFromBackup), - image: imageBackup, - title: S.of(context).restore_title_from_backup, - description: S.of(context).restore_description_from_backup), + onPressed: () async { + bool isPinSet = false; + if (isNewInstall) { + await Navigator.pushNamed(context, Routes.setupPin, + arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) { + setupPinContext.close(); + isPinSet = true; + }); + } + if (!isNewInstall || isPinSet) { + try { + final restoreWallet = + await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); + + final restoreFromQRViewModel = getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type); + + await restoreFromQRViewModel.create(restoreWallet: restoreWallet); + if (restoreFromQRViewModel.state is FailureState) { + _onWalletCreateFailure(context, + 'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}'); + } + } catch (e) { + _onWalletCreateFailure(context, e.toString()); + } + } + }, + image: qrCode, + title: S.of(context).scan_qr_code, + description: S.of(context).cold_or_recover_wallet), ) ], ), )), ); } + + void _onWalletCreateFailure(BuildContext context, String error) { + showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } } diff --git a/lib/src/screens/restore/sweeping_wallet_page.dart b/lib/src/screens/restore/sweeping_wallet_page.dart new file mode 100644 index 000000000..a7828b385 --- /dev/null +++ b/lib/src/screens/restore/sweeping_wallet_page.dart @@ -0,0 +1,123 @@ +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/scheduler.dart'; + +class SweepingWalletPage extends BasePage { + SweepingWalletPage(); + + static const aspectRatioImage = 1.25; + final welcomeImageLight = Image.asset('assets/images/welcome_light.png'); + final welcomeImageDark = Image.asset('assets/images/welcome.png'); + + + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + resizeToAvoidBottomInset: false, + body: body(context)); + } + + @override + Widget body(BuildContext context) { + final welcomeImage = currentTheme.type == ThemeType.dark ? welcomeImageDark : welcomeImageLight; + + return SweepingWalletWidget( + aspectRatioImage: aspectRatioImage, + welcomeImage: welcomeImage, + ); + } +} + +class SweepingWalletWidget extends StatefulWidget { + const SweepingWalletWidget({ + required this.aspectRatioImage, + required this.welcomeImage, + }); + + final double aspectRatioImage; + final Image welcomeImage; + + @override + State<SweepingWalletWidget> createState() => _SweepingWalletWidgetState(); +} + +class _SweepingWalletWidgetState extends State<SweepingWalletWidget> { + @override + void initState() { + SchedulerBinding.instance.addPostFrameCallback((_) async { + + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Container( + padding: EdgeInsets.only(top: 64, bottom: 24, left: 24, right: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: widget.aspectRatioImage, + child: FittedBox(child: widget.welcomeImage, fit: BoxFit.fill))), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Column( + children: <Widget>[ + Padding( + padding: EdgeInsets.only(top: 24), + child: Text( + S.of(context).please_wait, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.color!, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + S.of(context).sweeping_wallet, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryTextTheme!.headline6!.color!, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + S.of(context).sweeping_wallet_alert, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.color!, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + )) + ], + ))); + } +} + + diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 7288d624b..aa9d31f1b 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/core/seed_validator.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; class WalletRestorePage extends BasePage { WalletRestorePage(this.walletRestoreViewModel) diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 316203ddd..e1c4c48e5 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -178,7 +178,7 @@ class WalletListBodyState extends State<WalletListBody> { Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: widget.walletListViewModel.currentWalletType); } else { - Navigator.of(context).pushNamed(Routes.restoreWalletType); + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); } }, image: restoreWalletImage, diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index 86e1cbcf1..0249093bd 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -148,7 +148,8 @@ class WelcomePage extends BasePage { padding: EdgeInsets.only(top: 10), child: PrimaryImageButton( onPressed: () { - Navigator.pushNamed(context, Routes.restoreOptions); + Navigator.pushNamed(context, Routes.restoreOptions, + arguments: true); }, image: restoreWalletImage, text: S.of(context).restore_wallet, diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart new file mode 100644 index 000000000..7efb92e69 --- /dev/null +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -0,0 +1,106 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/core/generate_wallet_password.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; +import 'package:cw_core/wallet_info.dart'; + +part 'restore_from_qr_vm.g.dart'; + +class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRestorationFromQRVM; + +abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store { + WalletRestorationFromQRVMBase(AppStore appStore, WalletCreationService walletCreationService, + Box<WalletInfo> walletInfoSource, WalletType type) + : height = 0, + viewKey = '', + spendKey = '', + wif = '', + address = '', + super(appStore, walletInfoSource, walletCreationService, + type: type, isRecovery: true); + + @observable + int height; + + @observable + String viewKey; + + @observable + String spendKey; + + @observable + String wif; + + @observable + String address; + + bool get hasRestorationHeight => type == WalletType.monero; + + @override + WalletCredentials getCredentialsFromRestoredWallet(dynamic options, RestoredWallet restoreWallet) { + final password = generateWalletPassword(); + + switch (restoreWallet.restoreMode) { + case WalletRestoreMode.keys: + switch (restoreWallet.type) { + case WalletType.monero: + return monero!.createMoneroRestoreWalletFromKeysCredentials( + name: name, + password: password, + language: 'English', + address: restoreWallet.address ?? '', + viewKey: restoreWallet.viewKey ?? '', + spendKey: restoreWallet.spendKey ?? '', + height: restoreWallet.height ?? 0); + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials( + name: name, password: password, wif: wif); + default: + throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); + } + case WalletRestoreMode.seed: + switch (restoreWallet.type) { + case WalletType.monero: + return monero!.createMoneroRestoreWalletFromSeedCredentials( + name: name, + height: restoreWallet.height ?? 0, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password); + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + default: + throw Exception('Unexpected type: ${type.toString()}'); + } + default: + throw Exception('Unexpected type: ${type.toString()}'); + } + } + + @override + Future<WalletBase> processFromRestoredWallet(WalletCredentials credentials, RestoredWallet restoreWallet) async { + try { + switch (restoreWallet.restoreMode) { + case WalletRestoreMode.keys: + return walletCreationService.restoreFromKeys(credentials); + case WalletRestoreMode.seed: + return walletCreationService.restoreFromSeed(credentials); + default: + throw Exception('Unexpected restore mode: ${restoreWallet.restoreMode.toString()}'); + } + } catch (e) { + throw Exception('Unexpected restore mode: ${e.toString()}'); + } + } +} diff --git a/lib/view_model/restore/restore_mode.dart b/lib/view_model/restore/restore_mode.dart new file mode 100644 index 000000000..d8344841d --- /dev/null +++ b/lib/view_model/restore/restore_mode.dart @@ -0,0 +1 @@ +enum WalletRestoreMode { seed, keys, txids } \ No newline at end of file diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart new file mode 100644 index 000000000..0f872d8cc --- /dev/null +++ b/lib/view_model/restore/restore_wallet.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; +import 'package:cw_core/wallet_type.dart'; + +class RestoredWallet { + RestoredWallet( + {required this.restoreMode, + required this.type, + required this.address, + this.txId, + this.spendKey, + this.viewKey, + this.mnemonicSeed, + this.txAmount, + this.txDescription, + this.recipientName, + this.height}); + + final WalletRestoreMode restoreMode; + final WalletType type; + final String? address; + final String? txId; + final String? spendKey; + final String? viewKey; + final String? mnemonicSeed; + final String? txAmount; + final String? txDescription; + final String? recipientName; + final int? height; + + factory RestoredWallet.fromKey(Map<String, dynamic> json) { + final height = json['height'] as String?; + return RestoredWallet( + restoreMode: json['mode'] as WalletRestoreMode, + type: json['type'] as WalletType, + address: json['address'] as String?, + spendKey: json['spend_key'] as String?, + viewKey: json['view_key'] as String?, + height: height != null ? int.parse(height) : 0, + ); + } + + factory RestoredWallet.fromSeed(Map<String, dynamic> json) { + final height = json['height'] as String?; + final mnemonic_seed = json['mnemonic_seed'] as String?; + final seed = json['seed'] as String?; + return RestoredWallet( + restoreMode: json['mode'] as WalletRestoreMode, + type: json['type'] as WalletType, + address: json['address'] as String?, + mnemonicSeed: mnemonic_seed ?? seed, + height: height != null ? int.parse(height) : 0, + ); + } + + factory RestoredWallet.fromTxIds(Map<String, dynamic> json) { + return RestoredWallet( + restoreMode: json['mode'] as WalletRestoreMode, + type: json['type'] as WalletType, + address: json['address'] as String?, + txId: json['tx_payment_id'] as String, + txAmount: json['tx_amount'] as String, + txDescription: json['tx_description'] as String?, + recipientName: json['recipient_name'] as String?, + ); + } +} diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart new file mode 100644 index 000000000..241b2d3fd --- /dev/null +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -0,0 +1,159 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/seed_validator.dart'; +import 'package:cake_wallet/entities/mnemonic_item.dart'; +import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/cupertino.dart'; + +class WalletRestoreFromQRCode { + WalletRestoreFromQRCode(); + + static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async { + String code = await presentQRScanner(); + Map<String, dynamic> credentials = {}; + + if (code.isEmpty) { + throw Exception('Unexpected scan QR code value: value is empty'); + } + final formattedUri = getFormattedUri(code); + final uri = Uri.parse(formattedUri); + final queryParameters = uri.queryParameters; + credentials['type'] = getWalletTypeFromUrl(uri.scheme); + + final address = getAddressFromUrl( + type: credentials['type'] as WalletType, + rawString: queryParameters.toString(), + ); + if (address != null) { + credentials['address'] = address; + } + + final seed = + getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType); + if (seed != null) { + credentials['seed'] = seed; + } + credentials.addAll(queryParameters); + credentials['mode'] = getWalletRestoreMode(credentials); + + switch (credentials['mode']) { + case WalletRestoreMode.txids: + return RestoredWallet.fromTxIds(credentials); + case WalletRestoreMode.seed: + return RestoredWallet.fromSeed(credentials); + case WalletRestoreMode.keys: + return RestoredWallet.fromKey(credentials); + default: + throw Exception('Unexpected restore mode: ${credentials['mode']}'); + } + } + + static String getFormattedUri(String code) { + final index = code.indexOf(':'); + final scheme = code.substring(0, index).replaceAll('_', '-'); + final query = code.substring(index + 1).replaceAll('?', '&'); + final formattedUri = '$scheme:?$query'; + return formattedUri; + } + + static WalletType getWalletTypeFromUrl(String scheme) { + switch (scheme) { + case 'monero': + case 'monero-wallet': + return WalletType.monero; + case 'bitcoin': + case 'bitcoin-wallet': + return WalletType.bitcoin; + case 'litecoin': + case 'litecoin-wallet': + return WalletType.litecoin; + default: + throw Exception('Unexpected wallet type: ${scheme.toString()}'); + } + } + + static String? getAddressFromUrl({required WalletType type, required String rawString}) { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } + + static String? getSeedPhraseFromUrl(String rawString, WalletType walletType) { + switch (walletType) { + case WalletType.monero: + RegExp regex25 = RegExp(r'\b(\S+\b\s+){24}\S+\b'); + RegExp regex14 = RegExp(r'\b(\S+\b\s+){13}\S+\b'); + RegExp regex13 = RegExp(r'\b(\S+\b\s+){12}\S+\b'); + + if (regex25.firstMatch(rawString) == null) { + if (regex14.firstMatch(rawString) == null) { + if (regex13.firstMatch(rawString) == null) { + return null; + } else { + return regex13.firstMatch(rawString)!.group(0)!; + } + } else { + return regex14.firstMatch(rawString)!.group(0)!; + } + } else { + return regex25.firstMatch(rawString)!.group(0)!; + } + case WalletType.bitcoin: + case WalletType.litecoin: + RegExp regex24 = RegExp(r'\b(\S+\b\s+){23}\S+\b'); + RegExp regex18 = RegExp(r'\b(\S+\b\s+){17}\S+\b'); + RegExp regex12 = RegExp(r'\b(\S+\b\s+){11}\S+\b'); + + if (regex24.firstMatch(rawString) == null) { + if (regex18.firstMatch(rawString) == null) { + if (regex12.firstMatch(rawString) == null) { + return null; + } else { + return regex12.firstMatch(rawString)!.group(0)!; + } + } else { + return regex18.firstMatch(rawString)!.group(0)!; + } + } else { + return regex24.firstMatch(rawString)!.group(0)!; + } + default: + return null; + } + } + + static WalletRestoreMode getWalletRestoreMode(Map<String, dynamic> credentials) { + final type = credentials['type'] as WalletType; + if (credentials.containsKey('tx_payment_id')) { + final txIdValue = credentials['tx_payment_id'] as String? ?? ''; + return txIdValue.isNotEmpty + ? WalletRestoreMode.txids + : throw Exception('Unexpected restore mode: tx_payment_id is invalid'); + } + + if (credentials.containsKey('seed')) { + final seedValue = credentials['seed'] as String; + final words = SeedValidator.getWordList(type: type, language: 'english'); + seedValue.split(' ').forEach((element) { + if (!words.contains(element)) { + throw Exception('Unexpected restore mode: mnemonic_seed is invalid'); + } + }); + return WalletRestoreMode.seed; + } + + if (credentials.containsKey('spend_key') || credentials.containsKey('view_key')) { + final spendKeyValue = credentials['spend_key'] as String? ?? ''; + final viewKeyValue = credentials['view_key'] as String? ?? ''; + + return spendKeyValue.isNotEmpty || viewKeyValue.isNotEmpty + ? WalletRestoreMode.keys + : throw Exception('Unexpected restore mode: spend_key or view_key is invalid'); + } + + throw Exception('Unexpected restore mode: restore params are invalid'); + } +} diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 2609aed7d..323d1f911 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -39,7 +39,8 @@ abstract class WalletCreationVMBase with Store { bool typeExists(WalletType type) => walletCreationService.typeExists(type); - Future<void> create({dynamic options}) async { + Future<void> create({dynamic options, RestoredWallet? restoreWallet}) async { + final type = restoreWallet?.type ?? this.type; try { state = IsExecutingState(); if (name.isEmpty) { @@ -49,7 +50,9 @@ abstract class WalletCreationVMBase with Store { walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); - final credentials = getCredentials(options); + final credentials = restoreWallet != null + ? getCredentialsFromRestoredWallet(options, restoreWallet) + : getCredentials(options); final walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), name: name, @@ -62,7 +65,9 @@ abstract class WalletCreationVMBase with Store { address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven); credentials.walletInfo = walletInfo; - final wallet = await process(credentials); + final wallet = restoreWallet != null + ? await processFromRestoredWallet(credentials, restoreWallet) + : await process(credentials); walletInfo.address = wallet.walletAddresses.address; await _walletInfoSource.add(walletInfo); _appStore.changeCurrentWallet(wallet); @@ -72,10 +77,15 @@ abstract class WalletCreationVMBase with Store { state = FailureState(e.toString()); } } - WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future<WalletBase> process(WalletCredentials credentials) => throw UnimplementedError(); + + WalletCredentials getCredentialsFromRestoredWallet(dynamic options, RestoredWallet restoreWallet) => + throw UnimplementedError(); + + Future<WalletBase> processFromRestoredWallet(WalletCredentials credentials, RestoredWallet restoreWallet) => + throw UnimplementedError(); } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index e20089915..0a9ae60a7 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -88,16 +88,17 @@ abstract class WalletKeysViewModelBase with Store { return null; } - String get _path { + + String get _scheme { switch (_appStore.wallet!.type) { case WalletType.monero: - return 'monero_wallet:'; + return 'monero-wallet'; case WalletType.bitcoin: - return 'bitcoin_wallet:'; + return 'bitcoin-wallet'; case WalletType.litecoin: - return 'litecoin_wallet:'; + return 'litecoin-wallet'; case WalletType.haven: - return 'haven_wallet:'; + return 'haven-wallet'; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } @@ -124,7 +125,7 @@ abstract class WalletKeysViewModelBase with Store { Future<Uri> get url async { return Uri( - path: _path, + scheme: _scheme, queryParameters: await _queryParams, ); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index e505cff77..dcd57b3ff 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/view_model/wallet_restoration_from_keys_vm.dart b/lib/view_model/wallet_restoration_from_keys_vm.dart index f7195c240..97cb8d519 100644 --- a/lib/view_model/wallet_restoration_from_keys_vm.dart +++ b/lib/view_model/wallet_restoration_from_keys_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -66,7 +67,7 @@ abstract class WalletRestorationFromKeysVMBase extends WalletCreationVM return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials( name: name, password: password, wif: wif); default: - throw Exception('Unexpected type: ${type.toString()}');; + throw Exception('Unexpected type: ${type.toString()}'); } } diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart index ef584db08..0caa7f37d 100644 --- a/lib/view_model/wallet_restoration_from_seed_vm.dart +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index f122da3ba..7af653cf1 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/mnemonic_length.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -13,10 +14,10 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; part 'wallet_restore_view_model.g.dart'; -enum WalletRestoreMode { seed, keys } class WalletRestoreViewModel = WalletRestoreViewModelBase with _$WalletRestoreViewModel; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 23686e451..0caa02094 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -150,7 +150,7 @@ "receive_amount":"المقدار", "subaddresses":"العناوين الفرعية", "addresses":"عناوين", - "scan_qr_code":"امسح ال QR للحصول على العنوان", + "scan_qr_code_to_get_address":"امسح ال QR للحصول على العنوان", "qr_fullscreen":"انقر لفتح ال QR بملء الشاشة", "rename":"إعادة تسمية", "choose_account":"اختر حساب", @@ -683,6 +683,11 @@ "arrive_in_this_address" : "سيصل ${currency} ${tag}إلى هذا العنوان", "do_not_send": "لا ترسل", "error_dialog_content": "عفوًا ، لقد حصلنا على بعض الخطأ.\n\nيرجى إرسال تقرير التعطل إلى فريق الدعم لدينا لتحسين التطبيق.", + "scan_qr_code": "امسح رمز QR ضوئيًا", + "cold_or_recover_wallet": "أضف محفظة باردة أو استعد محفظة ورقية", + "please_wait": "انتظر من فضلك", + "sweeping_wallet": "كنس المحفظة", + "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلاً. لا تترك هذه الشاشة وإلا فقد يتم فقد أموال سويبت", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "edit_node": "تحرير العقدة", "frozen_balance": "الرصيد المجمد", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 9fd3432bd..930ed4bac 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -150,7 +150,7 @@ "receive_amount" : "Betrag", "subaddresses" : "Unteradressen", "addresses" : "Adressen", - "scan_qr_code" : "Scannen Sie den QR-Code, um die Adresse zu erhalten", + "scan_qr_code_to_get_address" : "Scannen Sie den QR-Code, um die Adresse zu erhalten", "qr_fullscreen" : "Tippen Sie hier, um den QR-Code im Vollbildmodus zu öffnen", "rename" : "Umbenennen", "choose_account" : "Konto auswählen", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}wird an dieser Adresse ankommen", "do_not_send": "Nicht senden", "error_dialog_content": "Hoppla, wir haben einen Fehler.\n\nBitte senden Sie den Absturzbericht an unser Support-Team, um die Anwendung zu verbessern.", + "scan_qr_code": "QR-Code scannen", + "cold_or_recover_wallet": "Fügen Sie eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her", + "please_wait": "Warten Sie mal", + "sweeping_wallet": "Kehre Geldbörse", + "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE SWEPT-GELDER VERLOREN GEHEN", "decimal_places_error": "Zu viele Nachkommastellen", "edit_node": "Knoten bearbeiten", "frozen_balance": "Gefrorenes Gleichgewicht", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7f03b96e9..22cc9b950 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -150,7 +150,7 @@ "receive_amount" : "Amount", "subaddresses" : "Subaddresses", "addresses" : "Addresses", - "scan_qr_code" : "Scan the QR code to get the address", + "scan_qr_code_to_get_address" : "Scan the QR code to get the address", "qr_fullscreen" : "Tap to open full screen QR code", "rename" : "Rename", "choose_account" : "Choose account", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}will arrive in this address", "do_not_send": "Don't send", "error_dialog_content": "Oops, we got some error.\n\nPlease send the crash report to our support team to make the application better.", + "scan_qr_code": "Scan QR code", + "cold_or_recover_wallet": "Add a cold wallet or recover a paper wallet", + "please_wait": "Please wait", + "sweeping_wallet": "Sweeping wallet", + "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.", "invoice_details": "Invoice details", "donation_link_details": "Donation link details", "anonpay_description": "Generate ${type}. The recipient can ${method} with any supported cryptocurrency, and you will receive funds in this wallet.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 4b649e12a..9159d7a12 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -150,7 +150,7 @@ "receive_amount" : "Cantidad", "subaddresses" : "Subdirecciones", "addresses" : "Direcciones", - "scan_qr_code" : "Escanee el código QR para obtener la dirección", + "scan_qr_code_to_get_address" : "Escanee el código QR para obtener la dirección", "qr_fullscreen" : "Toque para abrir el código QR en pantalla completa", "rename" : "Rebautizar", "choose_account" : "Elegir cuenta", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}llegará a esta dirección", "do_not_send": "no enviar", "error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.", + "scan_qr_code": "Escanear código QR", + "cold_or_recover_wallet": "Agregue una billetera fría o recupere una billetera de papel", + "please_wait": "Espere por favor", + "sweeping_wallet": "Billetera de barrido", + "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS", "decimal_places_error": "Demasiados lugares decimales", "edit_node": "Editar nodo", "frozen_balance": "Balance congelado", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 5250ed88d..983fc5f46 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -150,7 +150,7 @@ "receive_amount" : "Montant", "subaddresses" : "Sous-adresses", "addresses" : "Adresses", - "scan_qr_code" : "Scannez le QR code pour obtenir l'adresse", + "scan_qr_code_to_get_address" : "Scannez le QR code pour obtenir l'adresse", "qr_fullscreen" : "Appuyez pour ouvrir le QR code en mode plein écran", "rename" : "Renommer", "choose_account" : "Choisir le compte", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}arrivera à cette adresse", "do_not_send": "Ne pas envoyer", "error_dialog_content": "Oups, nous avons rencontré une erreur.\n\nMerci d'envoyer le rapport d'erreur à notre équipe d'assistance afin de nous permettre d'améliorer l'application.", + "scan_qr_code": "Scannez le code QR", + "cold_or_recover_wallet": "Ajoutez un cold wallet ou récupérez un paper wallet", + "please_wait": "S'il vous plaît, attendez", + "sweeping_wallet": "Portefeuille de balayage", + "sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN OU LES FONDS BALAYÉS POURRAIENT ÊTRE PERDUS", "decimal_places_error": "Trop de décimales", "edit_node": "Modifier le nœud", "frozen_balance": "Équilibre gelé", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 6a9a97880..8b505cff2 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -150,7 +150,7 @@ "receive_amount" : "रकम", "subaddresses" : "उप पते", "addresses" : "पतों", - "scan_qr_code" : "पता प्राप्त करने के लिए QR कोड स्कैन करें", + "scan_qr_code_to_get_address" : "पता प्राप्त करने के लिए QR कोड स्कैन करें", "qr_fullscreen" : "फ़ुल स्क्रीन क्यूआर कोड खोलने के लिए टैप करें", "rename" : "नाम बदलें", "choose_account" : "खाता चुनें", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}इस पते पर पहुंचेंगे", "do_not_send": "मत भेजो", "error_dialog_content": "ओह, हमसे कुछ गड़बड़ी हुई है.\n\nएप्लिकेशन को बेहतर बनाने के लिए कृपया क्रैश रिपोर्ट हमारी सहायता टीम को भेजें।", + "scan_qr_code": "स्कैन क्यू आर कोड", + "cold_or_recover_wallet": "कोल्ड वॉलेट जोड़ें या पेपर वॉलेट पुनर्प्राप्त करें", + "please_wait": "कृपया प्रतीक्षा करें", + "sweeping_wallet": "स्वीपिंग वॉलेट", + "sweeping_wallet_alert": "इसमें अधिक समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें या स्वैप्ट फंड खो सकते हैं", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "edit_node": "नोड संपादित करें", "frozen_balance": "जमे हुए संतुलन", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 10974efa2..f9b5fd0f2 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -150,7 +150,7 @@ "receive_amount" : "Iznos", "subaddresses" : "Podadrese", "addresses" : "Adrese", - "scan_qr_code" : "Skeniraj QR kod za dobivanje adrese", + "scan_qr_code_to_get_address" : "Skeniraj QR kod za dobivanje adrese", "qr_fullscreen" : "Dodirnite za otvaranje QR koda preko cijelog zaslona", "rename" : "Preimenuj", "choose_account" : "Odaberi račun", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}će stići na ovu adresu", "do_not_send": "Ne šalji", "error_dialog_content": "Ups, imamo grešku.\n\nPošaljite izvješće o padu našem timu za podršku kako bismo poboljšali aplikaciju.", + "scan_qr_code": "Skenirajte QR kod", + "cold_or_recover_wallet": "Dodajte hladni novčanik ili povratite papirnati novčanik", + "please_wait": "Molimo pričekajte", + "sweeping_wallet": "Čisti novčanik", + "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON INAČE SE POBREŠENA SREDSTVA MOGU IZGUBITI", "decimal_places_error": "Previše decimalnih mjesta", "edit_node": "Uredi čvor", "frozen_balance": "Zamrznuti saldo", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index b8881e2f6..95ce251bd 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -150,7 +150,7 @@ "receive_amount" : "Ammontare", "subaddresses" : "Sottoindirizzi", "addresses" : "Indirizzi", - "scan_qr_code" : "Scansiona il codice QR per ottenere l'indirizzo", + "scan_qr_code_to_get_address" : "Scansiona il codice QR per ottenere l'indirizzo", "qr_fullscreen" : "Tocca per aprire il codice QR a schermo intero", "rename" : "Rinomina", "choose_account" : "Scegli account", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}arriverà a questo indirizzo", "do_not_send": "Non inviare", "error_dialog_content": "Spiacenti, abbiamo riscontrato un errore.\n\nSi prega di inviare il rapporto sull'arresto anomalo al nostro team di supporto per migliorare l'applicazione.", + "scan_qr_code": "Scansiona il codice QR", + "cold_or_recover_wallet": "Aggiungi un cold wallet o recupera un paper wallet", + "please_wait": "Attendere prego", + "sweeping_wallet": "Portafoglio ampio", + "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPAZZATI POTREBBERO ANDARE PERSI", "decimal_places_error": "Troppe cifre decimali", "edit_node": "Modifica nodo", "frozen_balance": "Equilibrio congelato", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 97baf1281..c1b78cc64 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -150,7 +150,7 @@ "receive_amount" : "量", "subaddresses" : "サブアドレス", "addresses" : "住所", - "scan_qr_code" : "QRコードをスキャンして住所を取得します", + "scan_qr_code_to_get_address" : "QRコードをスキャンして住所を取得します", "qr_fullscreen" : "タップして全画面QRコードを開く", "rename" : "リネーム", "choose_account" : "アカウントを選択", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}はこの住所に到着します", "do_not_send": "送信しない", "error_dialog_content": "エラーが発生しました。\n\nアプリケーションを改善するために、クラッシュ レポートをサポート チームに送信してください。", + "scan_qr_code": "QRコードをスキャン", + "cold_or_recover_wallet": "コールド ウォレットを追加するか、ペーパー ウォレットを復元する", + "please_wait": "お待ちください", + "sweeping_wallet": "スイープウォレット", + "sweeping_wallet_alert": "これには時間がかかりません。この画面から離れないでください。そうしないと、スイープ ファンドが失われる可能性があります", "decimal_places_error": "小数点以下の桁数が多すぎる", "edit_node": "ノードを編集", "frozen_balance": "冷凍残高", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 50994a752..f2e03ed55 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -150,7 +150,7 @@ "receive_amount" : "양", "subaddresses" : "하위 주소", "addresses" : "구애", - "scan_qr_code" : "QR 코드를 스캔하여 주소를 얻습니다.", + "scan_qr_code_to_get_address" : "QR 코드를 스캔하여 주소를 얻습니다.", "qr_fullscreen" : "전체 화면 QR 코드를 열려면 탭하세요.", "rename" : "이름 바꾸기", "choose_account" : "계정을 선택하십시오", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}이(가) 이 주소로 도착합니다", "do_not_send": "보내지 마세요", "error_dialog_content": "죄송합니다. 오류가 발생했습니다.\n\n응용 프로그램을 개선하려면 지원 팀에 충돌 보고서를 보내주십시오.", + "scan_qr_code": "QR 코드 스캔", + "cold_or_recover_wallet": "콜드 지갑 추가 또는 종이 지갑 복구", + "please_wait": "기다리세요", + "sweeping_wallet": "스위핑 지갑", + "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마십시오. 그렇지 않으면 스웹트 자금이 손실될 수 있습니다.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "edit_node": "노드 편집", "frozen_balance": "얼어붙은 균형", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e93b8a0b9..c314dfb65 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -150,7 +150,7 @@ "receive_amount" : "ပမာဏ", "subaddresses" : "လိပ်စာများ", "addresses" : "လိပ်စာများ", - "scan_qr_code" : "လိပ်စာရယူရန် QR ကုဒ်ကို စကင်န်ဖတ်ပါ။", + "scan_qr_code_to_get_address" : "လိပ်စာရယူရန် QR ကုဒ်ကို စကင်န်ဖတ်ပါ။", "qr_fullscreen" : "မျက်နှာပြင်အပြည့် QR ကုဒ်ကိုဖွင့်ရန် တို့ပါ။", "rename" : "အမည်ပြောင်းပါ။", "choose_account" : "အကောင့်ကို ရွေးပါ။", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}ဤလိပ်စာသို့ ရောက်ရှိပါမည်။", "do_not_send": "မပို့ပါနှင့်", "error_dialog_content": "အိုး၊ ကျွန်ုပ်တို့တွင် အမှားအယွင်းအချို့ရှိသည်။\n\nအပလီကေးရှင်းကို ပိုမိုကောင်းမွန်စေရန်အတွက် ပျက်စီးမှုအစီရင်ခံစာကို ကျွန်ုပ်တို့၏ပံ့ပိုးကူညီရေးအဖွဲ့ထံ ပေးပို့ပါ။", + "scan_qr_code": "QR ကုဒ်ကို စကင်န်ဖတ်ပါ။", + "cold_or_recover_wallet": "အေးသောပိုက်ဆံအိတ်ထည့်ပါ သို့မဟုတ် စက္ကူပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", + "please_wait": "ကျေးဇူးပြုပြီးခဏစောင့်ပါ", + "sweeping_wallet": "ိုက်ဆံအိတ် တံမြက်လှည်း", + "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဤစခရင်ကို ချန်မထားပါနှင့် သို့မဟုတ် ထုတ်ယူထားသော ရန်ပုံငွေများ ဆုံးရှုံးနိုင်သည်", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "edit_node": "Node ကို တည်းဖြတ်ပါ။", "frozen_balance": "ေးခဲမှူ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 1561bb3d4..734630c96 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -151,7 +151,7 @@ "subaddresses" : "Subadressen", "rename" : "Hernoemen", "addresses" : "Adressen", - "scan_qr_code" : "Scan de QR-code om het adres te krijgen", + "scan_qr_code_to_get_address" : "Scan de QR-code om het adres te krijgen", "qr_fullscreen" : "Tik om de QR-code op volledig scherm te openen", "choose_account" : "Kies account", "create_new_account" : "Creëer een nieuw account", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}komt aan op dit adres", "do_not_send": "Niet sturen", "error_dialog_content": "Oeps, er is een fout opgetreden.\n\nStuur het crashrapport naar ons ondersteuningsteam om de applicatie te verbeteren.", + "scan_qr_code": "Scan QR-code", + "cold_or_recover_wallet": "Voeg een cold wallet toe of herstel een paper wallet", + "please_wait": "Even geduld aub", + "sweeping_wallet": "Vegende portemonnee", + "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KAN HET SWEPT-GELD VERLOREN WORDEN", "decimal_places_error": "Te veel decimalen", "edit_node": "Knooppunt bewerken", "frozen_balance": "Bevroren saldo", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index ea20d4da7..23b7bbb61 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -150,7 +150,7 @@ "receive_amount" : "Ilość", "subaddresses" : "Podadresy", "addresses" : "Adresy", - "scan_qr_code" : "Zeskanuj kod QR, aby uzyskać adres", + "scan_qr_code_to_get_address" : "Zeskanuj kod QR, aby uzyskać adres", "qr_fullscreen" : "Dotknij, aby otworzyć pełnoekranowy kod QR", "rename" : "Zmień nazwę", "choose_account" : "Wybierz konto", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}dotrze na ten adres", "do_not_send": "Nie wysyłaj", "error_dialog_content": "Ups, wystąpił błąd.\n\nPrześlij raport o awarii do naszego zespołu wsparcia, aby ulepszyć aplikację.", + "scan_qr_code": "Skanowania QR code", + "cold_or_recover_wallet": "Dodaj zimny portfel lub odzyskaj portfel papierowy", + "please_wait": "Proszę czekać", + "sweeping_wallet": "Zamiatanie portfela", + "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "decimal_places_error": "Za dużo miejsc dziesiętnych", "edit_node": "Edytuj węzeł", "frozen_balance": "Zamrożona równowaga", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 9f088fcef..0346b1ff4 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -150,7 +150,7 @@ "receive_amount" : "Quantia", "subaddresses" : "Sub-endereços", "addresses" : "Endereços", - "scan_qr_code" : "Digitalize o código QR para obter o endereço", + "scan_qr_code_to_get_address" : "Digitalize o código QR para obter o endereço", "qr_fullscreen" : "Toque para abrir o código QR em tela cheia", "rename" : "Renomear", "choose_account" : "Escolha uma conta", @@ -684,6 +684,11 @@ "arrive_in_this_address" : "${currency} ${tag}chegará neste endereço", "do_not_send": "não envie", "error_dialog_content": "Ops, houve algum erro.\n\nPor favor, envie o relatório de falha para nossa equipe de suporte para melhorar o aplicativo.", + "scan_qr_code": "Escanear código QR", + "cold_or_recover_wallet": "Adicione uma cold wallet ou recupere uma paper wallet", + "please_wait": "Por favor, aguarde", + "sweeping_wallet": "Carteira varrendo", + "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "decimal_places_error": "Muitas casas decimais", "edit_node": "Editar nó", "frozen_balance": "Saldo Congelado", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 53dc31b21..33058a13e 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -150,7 +150,7 @@ "receive_amount" : "Сумма", "subaddresses" : "Субадреса", "addresses" : "Адреса", - "scan_qr_code" : "Отсканируйте QR-код для получения адреса", + "scan_qr_code_to_get_address" : "Отсканируйте QR-код для получения адреса", "qr_fullscreen" : "Нажмите, чтобы открыть полноэкранный QR-код", "rename" : "Переименовать", "choose_account" : "Выберите аккаунт", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}придет на этот адрес", "do_not_send": "Не отправлять", "error_dialog_content": "Ой, у нас какая-то ошибка.\n\nПожалуйста, отправьте отчет о сбое в нашу службу поддержки, чтобы сделать приложение лучше.", + "scan_qr_code": "Сканировать QR-код", + "cold_or_recover_wallet": "Добавьте холодный кошелек или восстановите бумажный кошелек", + "please_wait": "Пожалуйста, подождите", + "sweeping_wallet": "Подметание кошелька", + "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ВЫЧИСЛЕННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ", "decimal_places_error": "Слишком много десятичных знаков", "edit_node": "Редактировать узел", "frozen_balance": "Замороженный баланс", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index ea7b2afde..f1cc320ac 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -150,7 +150,7 @@ "receive_amount" : "จำนวน", "subaddresses" : "ที่อยู่ย่อย", "addresses" : "ที่อยู่", - "scan_qr_code" : "สแกน QR code เพื่อรับที่อยู่", + "scan_qr_code_to_get_address" : "สแกน QR code เพื่อรับที่อยู่", "qr_fullscreen" : "แตะเพื่อเปิดหน้าจอ QR code แบบเต็มจอ", "rename" : "เปลี่ยนชื่อ", "choose_account" : "เลือกบัญชี", @@ -683,6 +683,11 @@ "arrive_in_this_address" : "${currency} ${tag}จะมาถึงที่อยู่นี้", "do_not_send": "อย่าส่ง", "error_dialog_content": "อ๊ะ เราพบข้อผิดพลาดบางอย่าง\n\nโปรดส่งรายงานข้อขัดข้องไปยังทีมสนับสนุนของเราเพื่อปรับปรุงแอปพลิเคชันให้ดียิ่งขึ้น", + "scan_qr_code": "สแกนรหัส QR", + "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินเย็นหรือกู้คืนกระเป๋าเงินกระดาษ", + "please_wait": "โปรดรอ", + "sweeping_wallet": "กวาดกระเป๋าสตางค์", + "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน อย่าออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดไปอาจสูญหาย", "decimal_places_error": "ทศนิยมมากเกินไป", "edit_node": "แก้ไขโหนด", "frozen_balance": "ยอดคงเหลือแช่แข็ง", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 7cb72e57a..675fbb5aa 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -150,7 +150,7 @@ "receive_amount" : "Miktar", "subaddresses" : "Alt adresler", "addresses" : "Adresler", - "scan_qr_code" : "Adresi getirmek için QR kodunu tara", + "scan_qr_code_to_get_address" : "Adresi getirmek için QR kodunu tara", "qr_fullscreen" : "QR kodunu tam ekranda açmak için dokun", "rename" : "Yeniden adlandır", "choose_account" : "Hesabı seç", @@ -685,6 +685,11 @@ "arrive_in_this_address" : "${currency} ${tag}bu adrese ulaşacak", "do_not_send": "Gönderme", "error_dialog_content": "Hay aksi, bir hatamız var.\n\nUygulamayı daha iyi hale getirmek için lütfen kilitlenme raporunu destek ekibimize gönderin.", + "scan_qr_code": "QR kodunu tarayın", + "cold_or_recover_wallet": "Soğuk bir cüzdan ekleyin veya bir kağıt cüzdanı kurtarın", + "please_wait": "Lütfen bekleyin", + "sweeping_wallet": "Süpürme cüzdanı", + "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN BIRAKMAYIN YOKSA SÜPÜRÜLEN FONLAR KAYBOLABİLİR", "decimal_places_error": "Çok fazla ondalık basamak", "edit_node": "Düğümü Düzenle", "frozen_balance": "Dondurulmuş Bakiye", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ac3d32dfd..affdda645 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -150,7 +150,7 @@ "receive_amount" : "Сума", "subaddresses" : "Субадреси", "addresses" : "Адреси", - "scan_qr_code" : "Скануйте QR-код для одержання адреси", + "scan_qr_code_to_get_address" : "Скануйте QR-код для одержання адреси", "qr_fullscreen" : "Торкніться, щоб відкрити QR-код на весь екран", "rename" : "Перейменувати", "choose_account" : "Оберіть акаунт", @@ -684,6 +684,11 @@ "arrive_in_this_address" : "${currency} ${tag}надійде на цю адресу", "do_not_send": "Не надсилайте", "error_dialog_content": "На жаль, ми отримали помилку.\n\nБудь ласка, надішліть звіт про збій нашій команді підтримки, щоб покращити додаток.", + "scan_qr_code": "Відскануйте QR-код", + "cold_or_recover_wallet": "Додайте холодний гаманець або відновіть паперовий гаманець", + "please_wait": "Будь ласка, зачекайте", + "sweeping_wallet": "Підмітаня гаманця", + "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЬОГО ЕКРАНУ, АБО КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ", "decimal_places_error": "Забагато знаків після коми", "edit_node": "Редагувати вузол", "frozen_balance": "Заморожений баланс", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ea1804b58..10a7d89f2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -150,7 +150,7 @@ "receive_amount" : "金额", "subaddresses" : "子地址", "addresses" : "地址", - "scan_qr_code" : "扫描二维码获取地址", + "scan_qr_code_to_get_address" : "扫描二维码获取地址", "qr_fullscreen" : "点击打开全屏二维码", "rename" : "重命名", "choose_account" : "选择账户", @@ -684,6 +684,11 @@ "arrive_in_this_address" : "${currency} ${tag}将到达此地址", "do_not_send": "不要发送", "error_dialog_content": "糟糕,我们遇到了一些错误。\n\n请将崩溃报告发送给我们的支持团队,以改进应用程序。", + "scan_qr_code": "扫描二维码", + "cold_or_recover_wallet": "添加冷钱包或恢复纸钱包", + "please_wait": "请稍等", + "sweeping_wallet": "扫一扫钱包", + "sweeping_wallet_alert": "\n这应该不会花很长时间。请勿离开此屏幕,否则可能会丢失所掠取的资金", "decimal_places_error": "小数位太多", "edit_node": "编辑节点", "frozen_balance": "冻结余额", From 9d47e0e67ca429fee6fde61ade98b1eb2e8ac4bd Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Mon, 24 Apr 2023 14:02:17 +0200 Subject: [PATCH 26/28] V4.6.3_1.3.3 (#893) * - Update App versions - Add release notes * Modify Monero.com release notes * Fix issues with Frozen balance for Monero.com build * Update cake wallet app build number --- assets/text/Monerocom_Release_Notes.txt | 26 ++++++++--------- assets/text/Release_Notes.txt | 29 ++++++++++--------- cw_core/lib/balance.dart | 2 ++ .../dashboard/balance_view_model.dart | 10 +------ .../restore/wallet_restore_from_qr_code.dart | 3 -- scripts/android/app_env.sh | 8 ++--- scripts/ios/app_env.sh | 6 ++-- scripts/macos/app_env.sh | 2 +- 8 files changed, 40 insertions(+), 46 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 6a091fc90..6c1994ede 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,13 +1,13 @@ -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges -WWEE(enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronization function -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronization function -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronizatio \ No newline at end of file +This update includes a HUGE number of usability improvements! +Type a fiat amount in the receive screen +Easily restore wallets from QR code +Pay Bitcoin Lightning invoices in exchange +Optionally disable the marketplace in display settings +Better warning messages if trying to exchange outside the min/max limits +More address flexibility when exchanging the currently-active wallet asset +Modernized the seed language selection picker +Adjusted pickers to resize if the keyboard is active +Improved accessibility +Click to copy additional fields in the exchange checkout +Fix padding on some devices +Bug fixes and code refactoring \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 6a091fc90..6e63d3372 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,13 +1,16 @@ -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges -WWEE(enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate)Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronization function -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronization function -Added Fixed Rate for exchanges (enter the "receive" amount on the exchange page to get the fixed rate) -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Changed algorithm for choosing of change address for BTC and LTC electrum wallets -Keep screen awake while the synchronizatio \ No newline at end of file +This update includes a HUGE number of usability improvements! +BTC/LTC coin control enhancements and bugfixes; easily look up Ordinals +Type a fiat amount in the receive screen +New Onramper Buy widget +Easily restore wallets from QR code +Substantially better reliability for seeing incoming, unconfirmed BTC/LTC transactions +Pay Bitcoin Lightning invoices in exchange +Optionally disable the marketplace in display settings +Better warning messages if trying to exchange outside the min/max limits +More address flexibility when exchanging the currently-active wallet asset +Modernized the seed language selection picker +Adjusted pickers to resize if the keyboard is active +Improved accessibility +Click to copy additional fields in the exchange checkout +Fix padding on some devices +Bug fixes and code refactoring \ No newline at end of file diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index cf98f9e0f..6145411c4 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -8,4 +8,6 @@ abstract class Balance { String get formattedAvailableBalance; String get formattedAdditionalBalance; + + String get formattedFrozenBalance => ''; } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 3b9dcce55..810d8e415 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,9 +1,5 @@ -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -15,7 +11,6 @@ import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; @@ -361,9 +356,6 @@ abstract class BalanceViewModelBase with Store { } } - - String getFormattedFrozenBalance(Balance walletBalance) => - walletBalance is ElectrumBalance ? walletBalance.formattedFrozenBalance : ''; - + String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedFrozenBalance; } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 241b2d3fd..9ebf01429 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,11 +1,8 @@ -import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/seed_validator.dart'; -import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 298bb69f4..fa822f0af 100644 --- 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.3.2" -MONERO_COM_BUILD_NUMBER=42 +MONERO_COM_VERSION="1.3.3" +MONERO_COM_BUILD_NUMBER=43 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.6.2" -CAKEWALLET_BUILD_NUMBER=151 +CAKEWALLET_VERSION="4.6.3" +CAKEWALLET_BUILD_NUMBER=154 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 3b700df46..ca2bf1f12 100644 --- 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.3.2" -MONERO_COM_BUILD_NUMBER=40 +MONERO_COM_VERSION="1.3.3" +MONERO_COM_BUILD_NUMBER=41 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.6.3" -CAKEWALLET_BUILD_NUMBER=146 +CAKEWALLET_BUILD_NUMBER=148 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 8cb2d86b9..d788fed1c 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,7 +16,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.0.2" -CAKEWALLET_BUILD_NUMBER=12 +CAKEWALLET_BUILD_NUMBER=14 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 82b513d1f8c0a253ede740843bd5c07f8d887c65 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Wed, 26 Apr 2023 14:59:27 +0200 Subject: [PATCH 27/28] V4.6.3 bug fixes (#896) * Add blocking screenshots native function to Monero.com [skip ci] * Fix seeds QR data * Update app versions and release notes [skip ci] * Update only build number for macos since it wasn't released yet [skip ci] * Fix adding sub-address * Fix Accounts Popup UI * Update app versions and release notes [skip ci] * Fix add/edit node issue * Update app versions and release notes [skip ci] * Fix input fields focus/keyboard issues * Update app versions and release notes [skip ci] --- .../java/com/monero/app/MainActivity.java | 11 +- assets/text/Monerocom_Release_Notes.txt | 17 +- assets/text/Release_Notes.txt | 20 +- ios/Podfile.lock | 6 + lib/src/screens/base_page.dart | 16 +- .../dashboard/widgets/address_page.dart | 33 ++- lib/src/screens/exchange/exchange_page.dart | 33 ++- .../monero_account_list_page.dart | 253 +++++++++--------- lib/src/screens/send/send_page.dart | 33 ++- .../address_edit_or_create_page.dart | 26 +- .../screens/wallet_keys/wallet_keys_page.dart | 4 +- scripts/android/app_env.sh | 8 +- scripts/ios/app_env.sh | 8 +- scripts/macos/app_env.sh | 2 +- 14 files changed, 282 insertions(+), 188 deletions(-) diff --git a/android/app/src/main/java/com/monero/app/MainActivity.java b/android/app/src/main/java/com/monero/app/MainActivity.java index f9e4f0882..73914c43c 100644 --- a/android/app/src/main/java/com/monero/app/MainActivity.java +++ b/android/app/src/main/java/com/monero/app/MainActivity.java @@ -23,6 +23,7 @@ import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; + boolean isAppSecure = false; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { @@ -48,13 +49,21 @@ public class MainActivity extends FlutterFragmentActivity { handler.post(() -> result.success(bytes)); break; case "getUnstoppableDomainAddress": - int version = Build.VERSION.SDK_INT; + int version = Build.VERSION.SDK_INT; if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { getUnstoppableDomainAddress(call, result); } else { handler.post(() -> result.success("")); } break; + case "setIsAppSecure": + isAppSecure = call.argument("isAppSecure"); + if (isAppSecure) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + } + break; default: handler.post(() -> result.notImplemented()); } diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 6c1994ede..d9acd464f 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,13 +1,4 @@ -This update includes a HUGE number of usability improvements! -Type a fiat amount in the receive screen -Easily restore wallets from QR code -Pay Bitcoin Lightning invoices in exchange -Optionally disable the marketplace in display settings -Better warning messages if trying to exchange outside the min/max limits -More address flexibility when exchanging the currently-active wallet asset -Modernized the seed language selection picker -Adjusted pickers to resize if the keyboard is active -Improved accessibility -Click to copy additional fields in the exchange checkout -Fix padding on some devices -Bug fixes and code refactoring \ No newline at end of file +Fix for QR codes +Fix for creating sub-addresses +Fix Add/Edit nodes +Fix issues with text/amount fields \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 6e63d3372..d9acd464f 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,16 +1,4 @@ -This update includes a HUGE number of usability improvements! -BTC/LTC coin control enhancements and bugfixes; easily look up Ordinals -Type a fiat amount in the receive screen -New Onramper Buy widget -Easily restore wallets from QR code -Substantially better reliability for seeing incoming, unconfirmed BTC/LTC transactions -Pay Bitcoin Lightning invoices in exchange -Optionally disable the marketplace in display settings -Better warning messages if trying to exchange outside the min/max limits -More address flexibility when exchanging the currently-active wallet asset -Modernized the seed language selection picker -Adjusted pickers to resize if the keyboard is active -Improved accessibility -Click to copy additional fields in the exchange checkout -Fix padding on some devices -Bug fixes and code refactoring \ No newline at end of file +Fix for QR codes +Fix for creating sub-addresses +Fix Add/Edit nodes +Fix issues with text/amount fields \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3a23117b2..d0e05f5dd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -115,6 +115,8 @@ PODS: - Flutter - flutter_secure_storage (3.3.1): - Flutter + - in_app_review (0.2.0): + - Flutter - local_auth_ios (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) @@ -165,6 +167,7 @@ DEPENDENCIES: - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) @@ -220,6 +223,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + in_app_review: + :path: ".symlinks/plugins/in_app_review/ios" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" package_info: @@ -260,6 +265,7 @@ SPEC CHECKSUMS: flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec + in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 28327ad39..51f8c1ad2 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; @@ -21,8 +20,6 @@ abstract class BasePage extends StatelessWidget { String? get title => null; - bool get canUseCloseIcon => false; - Color get backgroundLightColor => Colors.white; Color get backgroundDarkColor => PaletteDark.backgroundColor; @@ -53,27 +50,22 @@ abstract class BasePage extends StatelessWidget { final _backButton = Icon(Icons.arrow_back_ios, color: titleColor ?? Theme.of(context).primaryTextTheme.headline6!.color!, size: 16,); - final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme : closeButtonImage; - - bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); return MergeSemantics( child: SizedBox( - height: isMobileView ? 37 : 45, - width: isMobileView ? 37 : 45, + height: 37, + width: 37, child: ButtonTheme( minWidth: double.minPositive, child: Semantics( - label: canUseCloseIcon && !isMobileView ? 'Close' : 'Back', + label: 'Back', child: TextButton( style: ButtonStyle( overlayColor: MaterialStateColor.resolveWith( (states) => Colors.transparent), ), onPressed: () => onClose(context), - child: - canUseCloseIcon && !isMobileView ? _closeButton : _backButton, + child: _backButton, ), ), ), diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index cdaa22673..82430f0c6 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/share_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; @@ -62,7 +63,37 @@ class AddressPage extends BasePage { Color get titleColor => Colors.white; @override - bool get canUseCloseIcon => true; + Widget? leading(BuildContext context) { + final _backButton = Icon(Icons.arrow_back_ios, + color: titleColor, + size: 16, + ); + final _closeButton = currentTheme.type == ThemeType.dark + ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); + + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? 'Close' : 'Back', + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), + ), + ), + ), + ); + } @override Widget middle(BuildContext context) => diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index a434ed13b..fa3b3b825 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/debounce.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/sync_status.dart'; @@ -108,7 +109,37 @@ class ExchangePage extends BasePage { }); @override - bool get canUseCloseIcon => true; + Widget? leading(BuildContext context) { + final _backButton = Icon(Icons.arrow_back_ios, + color: titleColor, + size: 16, + ); + final _closeButton = currentTheme.type == ThemeType.dark + ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); + + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? 'Close' : 'Back', + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), + ), + ), + ), + ); + } @override Widget body(BuildContext context) { diff --git a/lib/src/screens/monero_accounts/monero_account_list_page.dart b/lib/src/screens/monero_accounts/monero_account_list_page.dart index 145a2d8a4..cb2fe0f2d 100644 --- a/lib/src/screens/monero_accounts/monero_account_list_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_list_page.dart @@ -1,9 +1,7 @@ -import 'dart:ui'; import 'package:cake_wallet/src/widgets/section_divider.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; @@ -36,131 +34,138 @@ class MoneroAccountListPage extends StatelessWidget { @override Widget build(BuildContext context) { return AlertBackground( - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: Text( - S.of(context).choose_account, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - fontFamily: 'Lato', - decoration: TextDecoration.none, - color: Colors.white - ), - ), - ), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 24), - child: GestureDetector( - onTap: () => null, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(14)), - child: Container( - height: 296, - color: Theme.of(context).textTheme!.headline1!.decorationColor!, - child: Column( - children: <Widget>[ - Expanded( - child: Observer( - builder: (_) { - final accounts = accountListViewModel.accounts; - isAlwaysShowScrollThumb = accounts == null - ? false - : accounts.length > 3; - - return Stack( - alignment: Alignment.center, - children: <Widget>[ - ListView.separated( - padding: EdgeInsets.zero, - controller: controller, - separatorBuilder: (context, index) => - const SectionDivider(), - itemCount: accounts.length ?? 0, - itemBuilder: (context, index) { - final account = accounts[index]; - - return AccountTile( - isCurrent: account.isSelected, - accountName: account.label, - onTap: () { - if (account.isSelected) { - return; - } - - accountListViewModel - .select(account); - Navigator.of(context).pop(); - }, - onEdit: () async => - await Navigator.of(context) - .pushNamed( - Routes.accountCreation, - arguments: account)); - }, - ), - isAlwaysShowScrollThumb - ? CakeScrollbar( - backgroundHeight: backgroundHeight, - thumbHeight: thumbHeight, - fromTop: accountListViewModel - .scrollOffsetFromTop - ) - : Offstage(), - ], - ); - } - ) - ), - GestureDetector( - onTap: () async => await Navigator.of(context) - .pushNamed(Routes.accountCreation), - child: Container( - height: 62, - color: Theme.of(context).cardColor, - padding: EdgeInsets.only(left: 24, right: 24), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Icon( - Icons.add, - color: Colors.white, - ), - Padding( - padding: EdgeInsets.only(left: 5), - child: Text( - S.of(context).create_new_account, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: Colors.white, - decoration: TextDecoration.none, - ), - ), - ) - ], - ), - ), - ), - ) - ], + child: Column( + children: [ + Expanded( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Text( + S.of(context).choose_account, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + decoration: TextDecoration.none, + color: Colors.white + ), ), ), - ), + Padding( + padding: EdgeInsets.only(left: 24, right: 24, top: 24), + child: GestureDetector( + onTap: () => null, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(14)), + child: Container( + height: 296, + color: Theme.of(context).textTheme!.headline1!.decorationColor!, + child: Column( + children: <Widget>[ + Expanded( + child: Observer( + builder: (_) { + final accounts = accountListViewModel.accounts; + isAlwaysShowScrollThumb = accounts == null + ? false + : accounts.length > 3; + + return Stack( + alignment: Alignment.center, + children: <Widget>[ + ListView.separated( + padding: EdgeInsets.zero, + controller: controller, + separatorBuilder: (context, index) => + const SectionDivider(), + itemCount: accounts.length ?? 0, + itemBuilder: (context, index) { + final account = accounts[index]; + + return AccountTile( + isCurrent: account.isSelected, + accountName: account.label, + onTap: () { + if (account.isSelected) { + return; + } + + accountListViewModel + .select(account); + Navigator.of(context).pop(); + }, + onEdit: () async => + await Navigator.of(context) + .pushNamed( + Routes.accountCreation, + arguments: account)); + }, + ), + isAlwaysShowScrollThumb + ? CakeScrollbar( + backgroundHeight: backgroundHeight, + thumbHeight: thumbHeight, + fromTop: accountListViewModel + .scrollOffsetFromTop + ) + : Offstage(), + ], + ); + } + ) + ), + GestureDetector( + onTap: () async => await Navigator.of(context) + .pushNamed(Routes.accountCreation), + child: Container( + height: 62, + color: Theme.of(context).cardColor, + padding: EdgeInsets.only(left: 24, right: 24), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Icon( + Icons.add, + color: Colors.white, + ), + Padding( + padding: EdgeInsets.only(left: 5), + child: Text( + S.of(context).create_new_account, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: Colors.white, + decoration: TextDecoration.none, + ), + ), + ) + ], + ), + ), + ), + ) + ], + ), + ), + ), + ), + ) + ], ), - ) - ], + SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), + AlertCloseButton() + ], + ), ), - AlertCloseButton() ], ), ); diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 544bed39c..ee0d8431c 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -53,7 +54,37 @@ class SendPage extends BasePage { bool get extendBodyBehindAppBar => true; @override - bool get canUseCloseIcon => true; + Widget? leading(BuildContext context) { + final _backButton = Icon(Icons.arrow_back_ios, + color: titleColor, + size: 16, + ); + final _closeButton = currentTheme.type == ThemeType.dark + ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = ResponsiveLayoutUtil.instance.isMobile(context); + + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? 'Close' : 'Back', + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), + ), + ), + ), + ); + } @override AppBarStyle get appBarStyle => AppBarStyle.transparent; diff --git a/lib/src/screens/subaddress/address_edit_or_create_page.dart b/lib/src/screens/subaddress/address_edit_or_create_page.dart index b7394182c..c0b003dec 100644 --- a/lib/src/screens/subaddress/address_edit_or_create_page.dart +++ b/lib/src/screens/subaddress/address_edit_or_create_page.dart @@ -1,5 +1,4 @@ import 'package:mobx/mobx.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -23,18 +22,14 @@ class AddressEditOrCreatePage extends BasePage { final GlobalKey<FormState> _formKey; final TextEditingController _labelController; + bool _isEffectsInstalled = false; + @override String get title => S.current.new_subaddress_title; @override Widget body(BuildContext context) { - reaction((_) => addressEditOrCreateViewModel.state, - (AddressEditOrCreateState state) { - if (state is AddressSavedSuccessfully) { - WidgetsBinding.instance - .addPostFrameCallback((_) => Navigator.of(context).pop()); - } - }); + _setEffects(context); return Form( key: _formKey, @@ -70,4 +65,19 @@ class AddressEditOrCreatePage extends BasePage { ), )); } + + void _setEffects(BuildContext context) { + if (_isEffectsInstalled) { + return; + } + reaction((_) => addressEditOrCreateViewModel.state, + (AddressEditOrCreateState state) { + if (state is AddressSavedSuccessfully) { + WidgetsBinding.instance + .addPostFrameCallback((_) => Navigator.of(context).pop()); + } + }); + + _isEffectsInstalled = true; + } } \ No newline at end of file diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index eb34393cf..da58c4b31 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -26,13 +25,14 @@ class WalletKeysPage extends BasePage { onPressed: () async { // Get the current brightness: final double brightness = await DeviceDisplayBrightness.getBrightness(); + final url = await walletKeysViewModel.url; // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(1.0); await Navigator.pushNamed( context, Routes.fullscreenQR, - arguments: QrViewData(data: await walletKeysViewModel.url.toString()), + arguments: QrViewData(data: url.toString()), ); // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(brightness); diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index fa822f0af..b08d55580 100644 --- 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.3.3" -MONERO_COM_BUILD_NUMBER=43 +MONERO_COM_VERSION="1.3.4" +MONERO_COM_BUILD_NUMBER=47 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.6.3" -CAKEWALLET_BUILD_NUMBER=154 +CAKEWALLET_VERSION="4.6.4" +CAKEWALLET_BUILD_NUMBER=158 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 ca2bf1f12..c874a14c3 100644 --- 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.3.3" -MONERO_COM_BUILD_NUMBER=41 +MONERO_COM_VERSION="1.3.4" +MONERO_COM_BUILD_NUMBER=45 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.6.3" -CAKEWALLET_BUILD_NUMBER=148 +CAKEWALLET_VERSION="4.6.4" +CAKEWALLET_BUILD_NUMBER=153 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index d788fed1c..76a32903b 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,7 +16,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.0.2" -CAKEWALLET_BUILD_NUMBER=14 +CAKEWALLET_BUILD_NUMBER=18 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 367efb3caed487ed10a3fe7b27ff568a00cf0c70 Mon Sep 17 00:00:00 2001 From: Omar Hatem <omarh.ismail1@gmail.com> Date: Thu, 27 Apr 2023 17:06:09 +0200 Subject: [PATCH 28/28] V4.6.4 fixes (#904) * Check if context is still mounted or not before showing popup * - Refactor Restore route flow - Fix Monero.com restore from QR - Remove deprecated restore classes - Update Monero.com app version and notes * Update Macos version and release notes * Fixate android plugin versions as Flutter published new fail packages version * Revert desktop changes as it's not supported yet to scan QR code on Desktop [skip ci] * Revert macos version update [skip ci] --- assets/text/Monerocom_Release_Notes.txt | 5 +- lib/router.dart | 101 +++------ lib/routes.dart | 9 +- .../desktop_wallet_selection_dropdown.dart | 1 - .../screens/restore/restore_options_page.dart | 3 +- .../restore_wallet_from_keys_page.dart | 212 ------------------ .../restore_wallet_from_seed_page.dart | 199 ---------------- .../screens/wallet_list/wallet_list_page.dart | 7 +- lib/utils/show_pop_up.dart | 37 +-- pubspec_base.yaml | 4 + scripts/android/app_env.sh | 4 +- scripts/ios/app_env.sh | 4 +- 12 files changed, 62 insertions(+), 524 deletions(-) delete mode 100644 lib/src/screens/restore/restore_wallet_from_keys_page.dart delete mode 100644 lib/src/screens/restore/restore_wallet_from_seed_page.dart diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index d9acd464f..be218630d 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,4 +1 @@ -Fix for QR codes -Fix for creating sub-addresses -Fix Add/Edit nodes -Fix issues with text/amount fields \ No newline at end of file +Fix Restore from QR code \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index 5a657a9ca..661a827e7 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -41,17 +41,14 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; -import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/utils/language_list.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; -import 'package:cake_wallet/view_model/wallet_restoration_from_keys_vm.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -65,9 +62,6 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; -import 'package:cake_wallet/src/screens/restore/restore_wallet_options_page.dart'; -import 'package:cake_wallet/src/screens/restore/restore_wallet_from_seed_page.dart'; -import 'package:cake_wallet/src/screens/restore/restore_wallet_from_keys_page.dart'; import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/seed_language/seed_language_page.dart'; @@ -144,14 +138,6 @@ Route<dynamic> createRoute(RouteSettings settings) { return CupertinoPageRoute<void>( builder: (_) => getIt.get<SetupPinCodePage>(param1: callback)); - case Routes.moneroRestoreWalletFromWelcome: - return CupertinoPageRoute<void>( - builder: (_) => getIt.get<SetupPinCodePage>( - param1: (PinCodeState<PinCodeWidget> context, dynamic _) => - Navigator.pushNamed( - context.context, Routes.restoreWallet, arguments: WalletType.monero)), - fullscreenDialog: true); - case Routes.restoreWalletType: return CupertinoPageRoute<void>( builder: (_) => getIt.get<NewWalletTypePage>( @@ -165,46 +151,35 @@ Route<dynamic> createRoute(RouteSettings settings) { return CupertinoPageRoute<void>( builder: (_) => getIt.get<RestoreOptionsPage>(param1: isNewInstall)); - case Routes.restoreWalletOptions: - final type = WalletType.monero; //settings.arguments as WalletType; - - return CupertinoPageRoute<void>( - builder: (_) => RestoreWalletOptionsPage( - type: type, - onRestoreFromSeed: (context) { - final route = type == WalletType.monero - ? Routes.seedLanguage - : Routes.restoreWalletFromSeed; - final args = type == WalletType.monero - ? [type, Routes.restoreWalletFromSeed] - : [type]; - - Navigator.of(context).pushNamed(route, arguments: args); - }, - onRestoreFromKeys: (context) { - final route = type == WalletType.monero - ? Routes.seedLanguage - : Routes.restoreWalletFromKeys; - final args = type == WalletType.monero - ? [type, Routes.restoreWalletFromKeys] - : [type]; - - Navigator.of(context).pushNamed(route, arguments: args); - })); - - case Routes.restoreWalletOptionsFromWelcome: + case Routes.restoreWalletFromSeedKeys: final isNewInstall = settings.arguments as bool; - return isNewInstall ? CupertinoPageRoute<void>( - builder: (_) => getIt.get<SetupPinCodePage>( - param1: (PinCodeState<PinCodeWidget> context, dynamic _) => - Navigator.pushNamed( - context.context, Routes.restoreWalletType)), - fullscreenDialog: true) : CupertinoPageRoute<void>( - builder: (_) => getIt.get<NewWalletTypePage>( - param1: (BuildContext context, WalletType type) => - Navigator.of(context) - .pushNamed(Routes.restoreWallet, arguments: type), - param2: false)); + + if (isNewInstall) { + return CupertinoPageRoute<void>( + builder: (_) => getIt.get<SetupPinCodePage>( + param1: (PinCodeState<PinCodeWidget> context, dynamic _) { + if (isSingleCoin) { + return Navigator.of(context.context) + .pushNamed(Routes.restoreWallet, arguments: availableWalletTypes.first); + } + + return Navigator.pushNamed( + context.context, Routes.restoreWalletType); + }), + fullscreenDialog: true); + } else if (isSingleCoin) { + return MaterialPageRoute<void>( + builder: (_) => getIt.get<WalletRestorePage>( + param1: availableWalletTypes.first + )); + } else { + return CupertinoPageRoute<void>( + builder: (_) => getIt.get<NewWalletTypePage>( + param1: (BuildContext context, WalletType type) => + Navigator.of(context) + .pushNamed(Routes.restoreWallet, arguments: type), + param2: false)); + } case Routes.seed: return MaterialPageRoute<void>( @@ -216,24 +191,6 @@ Route<dynamic> createRoute(RouteSettings settings) { builder: (_) => getIt.get<WalletRestorePage>( param1: settings.arguments as WalletType)); - case Routes.restoreWalletFromSeed: - final type = settings.arguments as WalletType; - return CupertinoPageRoute<void>( - builder: (_) => RestoreWalletFromSeedPage(type: type)); - - case Routes.restoreWalletFromKeys: - final args = settings.arguments as List<dynamic>; - final type = args.first as WalletType; - final language = - type == WalletType.monero ? args[1] as String : LanguageList.english; - - final walletRestorationFromKeysVM = - getIt.get<WalletRestorationFromKeysVM>(param1: [type, language]); - - return CupertinoPageRoute<void>( - builder: (_) => RestoreWalletFromKeysPage( - walletRestorationFromKeysVM: walletRestorationFromKeysVM)); - case Routes.sweepingWalletPage: return CupertinoPageRoute<void>( builder: (_) => getIt.get<SweepingWalletPage>()); diff --git a/lib/routes.dart b/lib/routes.dart index 823febe78..a0490ec28 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -3,14 +3,9 @@ class Routes { static const newWallet = '/new_wallet'; static const setupPin = '/setup_pin_code'; static const newWalletFromWelcome = '/new_wallet_from_welcome'; - static const restoreFromWelcome = '/restore_from_welcome'; static const seed = '/seed'; static const restoreOptions = '/restore_options'; - static const restoreOptionsFromWelcome = '/restore_options_from_welcome'; - static const restoreWalletOptions = '/restore_seed_keys'; - static const restoreWalletOptionsFromWelcome = '/restore_wallet_options_from_welcome'; - static const restoreWalletFromSeed = '/restore_wallet_from_seed'; - static const restoreWalletFromKeys = '/restore_wallet_from_keys'; + static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; static const dashboard = '/dashboard'; static const send = '/send'; static const transactionDetails = '/transaction_info'; @@ -57,8 +52,6 @@ class Routes { static const buyWebView = '/buy_web_view'; static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; - static const moneroRestoreWalletFromWelcome = '/monero_restore_wallet'; - static const moneroNewWalletFromWelcome = '/monero_new_wallet'; static const addressPage = '/address_page'; static const fullscreenQR = '/fullscreen_qr'; static const ioniaWelcomePage = '/cake_pay_welcome_page'; diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 0bfcb359e..a8f050002 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -1,6 +1,5 @@ import 'package:another_flushbar/flushbar.dart'; import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 8025ebd85..c08d2b7e4 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/cupertino.dart'; @@ -38,7 +39,7 @@ class RestoreOptionsPage extends BasePage { children: <Widget>[ RestoreButton( onPressed: () => Navigator.pushNamed( - context, Routes.restoreWalletOptionsFromWelcome, + context, Routes.restoreWalletFromSeedKeys, arguments: isNewInstall), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, diff --git a/lib/src/screens/restore/restore_wallet_from_keys_page.dart b/lib/src/screens/restore/restore_wallet_from_keys_page.dart deleted file mode 100644 index 85243870a..000000000 --- a/lib/src/screens/restore/restore_wallet_from_keys_page.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/blockchain_height_widget.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/wallet_restoration_from_keys_vm.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; - -class RestoreWalletFromKeysPage extends BasePage { - RestoreWalletFromKeysPage( - {required this.walletRestorationFromKeysVM}); - - final WalletRestorationFromKeysVM walletRestorationFromKeysVM; - - @override - String get title => S.current.restore_title_from_keys; - - @override - Widget body(BuildContext context) => RestoreFromKeysFrom(walletRestorationFromKeysVM); -} - -class RestoreFromKeysFrom extends StatefulWidget { - RestoreFromKeysFrom(this.walletRestorationFromKeysVM); - - final WalletRestorationFromKeysVM walletRestorationFromKeysVM; - - @override - _RestoreFromKeysFromState createState() => _RestoreFromKeysFromState(); -} - -class _RestoreFromKeysFromState extends State<RestoreFromKeysFrom> { - final _formKey = GlobalKey<FormState>(); - final _blockchainHeightKey = GlobalKey<BlockchainHeightState>(); - final _nameController = TextEditingController(); - final _addressController = TextEditingController(); - final _viewKeyController = TextEditingController(); - final _spendKeyController = TextEditingController(); - final _wifController = TextEditingController(); - - @override - void initState() { - _nameController.addListener(() => - widget.walletRestorationFromKeysVM.name = _nameController.text); - _addressController.addListener(() => - widget.walletRestorationFromKeysVM.address = _addressController.text); - _viewKeyController.addListener(() => - widget.walletRestorationFromKeysVM.viewKey = _viewKeyController.text); - _spendKeyController.addListener(() => - widget.walletRestorationFromKeysVM.spendKey = _spendKeyController.text); - _wifController.addListener(() => - widget.walletRestorationFromKeysVM.wif = _wifController.text); - - super.initState(); - } - - @override - void dispose() { - _nameController.dispose(); - _addressController.dispose(); - _viewKeyController.dispose(); - _spendKeyController.dispose(); - _wifController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - - /*reaction((_) => walletRestorationStore.state, (WalletRestorationState state) { - if (state is WalletRestoredSuccessfully) { - Navigator.of(context).popUntil((route) => route.isFirst); - } - - if (state is WalletRestorationFailure) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.restore_title_from_keys, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); - }); - }); - } - });*/ - - return Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24.0), - content: Form( - key: _formKey, - child: Column(children: <Widget>[ - Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: BaseTextFormField( - controller: _nameController, - hintText: S.of(context).restore_wallet_name, - validator: WalletNameValidator(), - ) - )) - ], - ), - if (!widget.walletRestorationFromKeysVM.hasRestorationHeight) - Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: BaseTextFormField( - controller: _wifController, - hintText: 'WIF', - ) - )) - ], - ), - if (widget.walletRestorationFromKeysVM.hasRestorationHeight) ... [ - Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: BaseTextFormField( - controller: _addressController, - keyboardType: TextInputType.multiline, - maxLines: null, - hintText: S.of(context).restore_address, - ) - )) - ], - ), - Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: BaseTextFormField( - controller: _viewKeyController, - hintText: S.of(context).restore_view_key_private, - ) - )) - ], - ), - Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: BaseTextFormField( - controller: _spendKeyController, - hintText: S.of(context).restore_spend_key_private, - ) - )) - ], - ), - BlockchainHeightWidget( - key: _blockchainHeightKey, - onHeightChange: (height) { - widget.walletRestorationFromKeysVM.height = height; - print(height); - }), - Padding( - padding: EdgeInsets.only(left: 40, right: 40, top: 24), - child: Text( - S.of(context).restore_from_date_or_blockheight, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context).hintColor - ), - ), - )], - ]), - ), - bottomSectionPadding: EdgeInsets.only(bottom: 24), - bottomSection: Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () { - if (_formKey.currentState != null && _formKey.currentState!.validate()) { - /*walletRestorationStore.restoreFromKeys( - name: _nameController.text, - language: seedLanguageStore.selectedSeedLanguage, - address: _addressController.text, - viewKey: _viewKeyController.text, - spendKey: _spendKeyController.text, - restoreHeight: _blockchainHeightKey.currentState.height);*/ - } - }, - text: S.of(context).restore_recover, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, - textColor: Colors.white, - //isDisabled: walletRestorationStore.disabledState, - ); - }), - ), - ); - } -} diff --git a/lib/src/screens/restore/restore_wallet_from_seed_page.dart b/lib/src/screens/restore/restore_wallet_from_seed_page.dart deleted file mode 100644 index 31a854049..000000000 --- a/lib/src/screens/restore/restore_wallet_from_seed_page.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:cake_wallet/src/screens/restore/restore_from_keys.dart'; -import 'package:cake_wallet/src/screens/restore/wallet_restore_from_seed_form.dart'; -import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/blockchain_height_widget.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/utils/show_pop_up.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/seed_widget.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/core/seed_validator.dart'; -import 'package:cake_wallet/core/mnemonic_length.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; - -class RestoreWalletFromSeedPage extends BasePage { - RestoreWalletFromSeedPage({required this.type}) - : _pages = <Widget>[]; - - final WalletType type; - final String language = 'en'; - - // final formKey = GlobalKey<_RestoreFromSeedFormState>(); - // final formKey = GlobalKey<_RestoreFromSeedFormState>(); - - @override - String get title => S.current.restore_title_from_seed; - - final controller = PageController(initialPage: 0); - List<Widget> _pages; - - Widget _page(BuildContext context, int index) { - if (_pages == null || _pages.isEmpty) { - _setPages(context); - } - - return _pages[index]; - } - - int _pageLength(BuildContext context) { - if (_pages == null || _pages.isEmpty) { - _setPages(context); - } - - return _pages.length; - } - - void _setPages(BuildContext context) { - _pages = <Widget>[ - // FIX-ME: Added args (displayBlockHeightSelector: true, displayLanguageSelector: true, type: type) - WalletRestoreFromSeedForm(displayBlockHeightSelector: true, displayLanguageSelector: true, type: type), - RestoreFromKeysFrom(), - ]; - } - - @override - Widget body(BuildContext context) { - return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: PageView.builder( - onPageChanged: (page) { - print('Page index $page'); - }, - controller: controller, - itemCount: _pageLength(context), - itemBuilder: (context, index) => _page(context, index))), - Padding( - padding: EdgeInsets.only(top: 10), - child: SmoothPageIndicator( - controller: controller, - count: _pageLength(context), - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).hintColor.withOpacity(0.5), - activeDotColor: Theme.of(context).hintColor), - )), - Padding( - padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), - child: PrimaryButton( - text: S.of(context).restore_recover, - isDisabled: false, - onPressed: () => null, - color: Theme.of(context).accentTextTheme!.bodyText1!.color!, - textColor: Colors.white)), - ]); - - // return GestureDetector( - // onTap: () => - // SystemChannels.textInput.invokeMethod<void>('TextInput.hide'), - // child: ScrollableWithBottomSection( - // bottomSection: Column(children: [ - // GestureDetector( - // onTap: () {}, - // child: Text('Switch to restore from keys', - // style: TextStyle(fontSize: 15, color: Theme.of(context).hintColor))), - // SizedBox(height: 30), - // PrimaryButton( - // text: S.of(context).restore_next, - // isDisabled: false, - // onPressed: () => null, - // color: Theme.of(context).accentTextTheme!.bodyText1!.color!, - // textColor: Colors.white) - // ]), - // contentPadding: EdgeInsets.only(bottom: 24), - // content: Container( - // padding: EdgeInsets.only(left: 25, right: 25), - // child: Column(children: [ - // SeedWidget( - // maxLength: mnemonicLength(type), - // onMnemonicChange: (seed) => null, - // onFinish: () => Navigator.of(context).pushNamed( - // Routes.restoreWalletFromSeedDetails, - // arguments: [type, language, '']), - // validator: SeedValidator(type: type, language: language), - // ), - // // SizedBox(height: 15), - // // BaseTextFormField(hintText: 'Language', initialValue: 'English'), - // BlockchainHeightWidget( - // // key: _blockchainHeightKey, - // onHeightChange: (height) { - // // widget.walletRestorationFromKeysVM.height = height; - // print(height); - // }) - // ]))), - // ); - } -} - -class RestoreFromSeedForm extends StatefulWidget { - RestoreFromSeedForm( - {Key? key, - required this.type, - this.language, - this.leading, - this.middle}) - : super(key: key); - final WalletType type; - final String? language; - final Widget? leading; - final Widget? middle; - - @override - _RestoreFromSeedFormState createState() => _RestoreFromSeedFormState(); -} - -class _RestoreFromSeedFormState extends State<RestoreFromSeedForm> { - // final _seedKey = GlobalKey<SeedWidgetState>(); - - String mnemonic() => - ''; // _seedKey.currentState.items.map((e) => e.text).join(' '); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => - SystemChannels.textInput.invokeMethod<void>('TextInput.hide'), - child: Container( - padding: EdgeInsets.only(left: 24, right: 24), - // color: Colors.blue, - // height: 300, - child: Column(children: [ - SeedWidget( - type: widget.type, - language: widget.language ?? '', - // key: _seedKey, - // maxLength: mnemonicLength(widget.type), - // onMnemonicChange: (seed) => null, - // onFinish: () => Navigator.of(context).pushNamed( - // Routes.restoreWalletFromSeedDetails, - // arguments: [widget.type, widget.language, mnemonic()]), - // leading: widget.leading, - // middle: widget.middle, - // validator: - // SeedValidator(type: widget.type, language: widget.language), - ), - BlockchainHeightWidget( - // key: _blockchainHeightKey, - onHeightChange: (height) { - // widget.walletRestorationFromKeysVM.height = height; - print(height); - }), - Container( - color: Colors.green, - width: 100, - height: 56, - child: BaseTextFormField( - hintText: 'Language', initialValue: 'English')), - ])), - ); - } -} diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index e1c4c48e5..bf36b129b 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -174,12 +174,7 @@ class WalletListBodyState extends State<WalletListBody> { SizedBox(height: 10.0), PrimaryImageButton( onPressed: () { - if (isSingleCoin) { - Navigator.of(context).pushNamed(Routes.restoreWallet, - arguments: widget.walletListViewModel.currentWalletType); - } else { - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); - } + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); }, image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, diff --git a/lib/utils/show_pop_up.dart b/lib/utils/show_pop_up.dart index 190b2a6d7..76114cc80 100644 --- a/lib/utils/show_pop_up.dart +++ b/lib/utils/show_pop_up.dart @@ -1,20 +1,23 @@ import 'package:flutter/material.dart'; -Future<T?> showPopUp<T>({ - required BuildContext context, - required WidgetBuilder builder, - bool barrierDismissible = true, - Color? barrierColor, - bool useSafeArea = false, - bool useRootNavigator = true, - RouteSettings? routeSettings -}) { - return showDialog<T>( - context: context, - builder: builder, - barrierDismissible: barrierDismissible, - barrierColor: barrierColor, - useSafeArea: useSafeArea, - useRootNavigator: useRootNavigator, - routeSettings: routeSettings); +Future<T?> showPopUp<T>( + {required BuildContext context, + required WidgetBuilder builder, + bool barrierDismissible = true, + Color? barrierColor, + bool useSafeArea = false, + bool useRootNavigator = true, + RouteSettings? routeSettings}) async { + if (context.mounted) { + return showDialog<T>( + context: context, + builder: builder, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + useSafeArea: useSafeArea, + useRootNavigator: useRootNavigator, + routeSettings: routeSettings); + } + + return null; } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 8beb79116..58de6edab 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -73,6 +73,10 @@ dependencies: url: https://github.com/cake-tech/cake_backup.git ref: main version: 1.0.0 + flutter_plugin_android_lifecycle: 2.0.9 + path_provider_android: 2.0.24 + shared_preferences_android: 2.0.17 + url_launcher_android: 6.0.24 dev_dependencies: flutter_test: diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index b08d55580..72d3aabfc 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,8 +14,8 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.4" -MONERO_COM_BUILD_NUMBER=47 +MONERO_COM_VERSION="1.3.5" +MONERO_COM_BUILD_NUMBER=48 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index c874a14c3..412ec5aa9 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,8 +13,8 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.4" -MONERO_COM_BUILD_NUMBER=45 +MONERO_COM_VERSION="1.3.5" +MONERO_COM_BUILD_NUMBER=46 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet"