From e5a5d5ef9756e39bc53e3c8760b0511a8aadb8d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Jan 2026 14:44:59 +0300 Subject: [PATCH 01/28] Updated on 2026-08-14 --- .bundle/config | 2 - .gitignore | 9 -- Gemfile | 6 - Gemfile.lock | 220 -------------------------- app/src/main/assets/tangem-app-config | 2 +- fastlane/Appfile | 2 - fastlane/Fastfile | 69 -------- fastlane/Pluginfile | 5 - fastlane/README.md | 66 -------- tangem-android-tools | 2 +- 10 files changed, 2 insertions(+), 381 deletions(-) delete mode 100644 .bundle/config delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100644 fastlane/Appfile delete mode 100644 fastlane/Fastfile delete mode 100644 fastlane/Pluginfile delete mode 100644 fastlane/README.md diff --git a/.bundle/config b/.bundle/config deleted file mode 100644 index 2369228816..0000000000 --- a/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_PATH: "vendor/bundle" diff --git a/.gitignore b/.gitignore index 345207b4eb..c5484c9083 100644 --- a/.gitignore +++ b/.gitignore @@ -38,19 +38,10 @@ app/src/external/google-services.json # LayoutInspector files /captures -# bundler -/vendor - # OS-specific files .DS_Store .DS_Store? -# fastlane files -**/fastlane/report.xml -**/fastlane/Preview.html -**/fastlane/screenshots -**/fastlane/test_output - # Kotlin Plugin .kotlin/ find-latest-release-branch.output diff --git a/Gemfile b/Gemfile deleted file mode 100644 index cdd3a6b349..0000000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 535cdb9a32..0000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,220 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.6) - rexml - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) - atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.702.0) - aws-sdk-core (3.170.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.62.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.119.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - claide (1.1.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - declarative (0.0.20) - digest-crc (0.6.4) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.98.0) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.0) - faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.211.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) - naturally (~> 2.2) - optparse (~> 0.1.1) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.4.2) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.33.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-core (0.10.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.16.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-playcustomapp_v1 (0.12.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) - google-cloud-storage (1.44.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.3.0) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.5) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.2) - json (2.6.3) - jwt (2.6.0) - memoist (0.16.2) - mini_magick (4.12.0) - mini_mime (1.1.2) - multi_json (1.15.0) - multipart-post (2.0.0) - nanaimo (0.3.0) - naturally (2.2.1) - optparse (0.1.1) - os (1.1.4) - plist (3.6.0) - public_suffix (5.0.1) - rake (13.0.6) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.2.5) - rouge (2.0.7) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.3) - signet (0.17.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.1) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.8.1) - word_wrap (1.0.0) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-firebase_app_distribution - -BUNDLED WITH - 2.5.23 diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 098662beb5..8285f8d289 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 098662beb5b0123b11f5ee4873d4bd667ac93c73 +Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304 diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index 1766aa8405..0000000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,2 +0,0 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -package_name("com.tangem.wallet") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index f1162f841b..0000000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,69 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:android) - -platform :android do - - desc "Run detekt" - lane :detekt do - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") - puts File.read("../gradle.properties") - gradle(task: "detekt detektMain") - end - - desc "Run tests" - lane :test do - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") - puts File.read("../gradle.properties") - gradle(task: "unitTest") - end - - desc "Build internal APK Firebase App Distribution" - lane :buildInternal do |options| - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../tangem-android-tools/CI/gradle_properties/build_ci_gradle.properties", "../gradle.properties") - puts File.read("../gradle.properties") - - gradle( - task: "assemble", - build_type: "GoogleInternal", - properties: { - 'versionCode' => ENV['version_code'], - 'versionName' => ENV['version_name'], - } - ) - end - - desc "Publish internal build to Firebase App Distribution" - lane :publishToFirebase do |options| - uploaded_release = firebase_app_distribution( - firebase_cli_token: ENV['firebase_cli_token'], - app: ENV['app_id_internal'], - apk_path: ENV['apk_path_internal'], - groups: ENV['groups'], - release_notes: ENV['release_notes'], - ) - release_url = uploaded_release[:testingUri] - - # Applying base64 encoding twice to prevent GA bug "Skip output 'output' since it may contain secret." - # See https://github.com/orgs/community/discussions/13082#discussioncomment-6776428 for details - encoded_release_url = Base64.strict_encode64(Base64.strict_encode64(release_url)) - sh("echo 'encoded_release_url=#{encoded_release_url}' >> /workspace/github_output.txt") - end - -end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile deleted file mode 100644 index b18539bc9b..0000000000 --- a/fastlane/Pluginfile +++ /dev/null @@ -1,5 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-firebase_app_distribution' diff --git a/fastlane/README.md b/fastlane/README.md deleted file mode 100644 index 44d6ef904a..0000000000 --- a/fastlane/README.md +++ /dev/null @@ -1,66 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## Android - -### android test - -```sh -[bundle exec] fastlane android test -``` - -Runs all the tests - -### android release - -```sh -[bundle exec] fastlane android release -``` - -Build a signed release APK - -### android build - -```sh -[bundle exec] fastlane android build -``` - -Build external and release APKs - -### android beta - -```sh -[bundle exec] fastlane android beta -``` - -Submit a new Beta Build to Firebase App Distribution - -### android publishToFirebase - -```sh -[bundle exec] fastlane android publishToFirebase -``` - -Publish internal and external builds to Firebase App Distribution - - - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index d7950d60bb..28079b094a 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit d7950d60bb4c6353f2aa3364073c6ec72167c666 +Subproject commit 28079b094a4518de068161226576747089519918 From 25f31f5f4d1790e448ab7a0389c3b3d07898e60c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 13:20:42 +0400 Subject: [PATCH 02/28] Updated on 2026-08-14 --- .../approve/model/YieldSupplyApproveModel.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 88123f909a..cc24881811 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory @@ -58,6 +59,7 @@ internal class YieldSupplyApproveModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val yieldSupplyGetContractAddressUseCase: YieldSupplyGetContractAddressUseCase, + private val yieldSupplyPendingTracker: YieldSupplyPendingTracker, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { @@ -157,7 +159,12 @@ internal class YieldSupplyApproveModel @Inject constructor( ) params.callback.onTransactionProgress(false) }, - ifRight = { + ifRight = { txHash -> + yieldSupplyPendingTracker.addPending( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + txIds = listOf(txHash), + ) val event = AnalyticsParam.TxSentFrom.Earning( blockchain = cryptoCurrency.network.name, token = cryptoCurrency.symbol, From 2e59b7e1161e9e83faf4ae28e1bfd9ceedaade26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 13:55:43 +0400 Subject: [PATCH 03/28] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../tap/di/domain/TransactionDomainModule.kt | 14 ++ core/res/src/main/res/values-es/strings.xml | 34 +++ core/res/src/main/res/values-fr/strings.xml | 49 +++- core/res/src/main/res/values-ja/strings.xml | 59 ++++- core/res/src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 13 ++ .../tokens/model/details/NavigationAction.kt | 1 + .../error/SignCloreMessageError.kt | 10 + .../usecase/SignCloreMessageUseCase.kt | 58 +++++ features/tokendetails/impl/build.gradle.kts | 1 + .../tokendetails/model/CloreMigrationModel.kt | 113 +++++++++ .../model/TokenDetailsClickIntents.kt | 15 +- .../tokendetails/model/TokenDetailsModel.kt | 59 ++++- .../components/TokenDetailsNotification.kt | 10 +- .../TokenDetailsNotificationConverter.kt | 4 +- .../state/factory/TokenDetailsStateFactory.kt | 59 +++++ .../tokendetails/ui/TokenDetailsScreen.kt | 34 ++- .../clore/CloreMigrationBottomSheet.kt | 220 ++++++++++++++++++ .../clore/CloreMigrationBottomSheetConfig.kt | 15 ++ .../intents/WalletContentClickIntents.kt | 2 + .../intents/WalletWarningsClickIntents.kt | 14 ++ .../utils/WalletWarningsAnalyticsSender.kt | 1 + .../domain/GetMultiWalletWarningsFactory.kt | 24 ++ .../wallet/state/model/WalletNotification.kt | 14 ++ gradle/tangem_dependencies.toml | 2 +- tangem-android-tools | 2 +- 28 files changed, 802 insertions(+), 38 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8285f8d289..098662beb5 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304 +Subproject commit 098662beb5b0123b11f5ee4873d4bd667ac93c73 diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8965f60090..5ce513772f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -265,4 +265,18 @@ internal object TransactionDomainModule { ): SendLargeSolanaTransactionUseCase { return SendLargeSolanaTransactionUseCase(cardSdkConfigRepository, walletManagersFacade) } + + @Provides + @Singleton + fun provideSignCloreMessageUseCase( + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, + ): SignCloreMessageUseCase { + return SignCloreMessageUseCase( + walletManagersFacade = walletManagersFacade, + cardSdkConfigRepository = cardSdkConfigRepository, + getHotWalletSigner = tangemHotWalletSignerFactory::create, + ) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 901dbaefc1..124b0453e1 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -299,6 +299,7 @@ Ir al token Entendido Ocultar + Mantener para %s hora Importe En progreso @@ -323,6 +324,7 @@ No Ninguna dirección No agregadas + No disponible Ahora no Ahora OK @@ -373,6 +375,7 @@ Intercambiar Tangem Tangem Wallet + Toque y mantenga presionado términos y condiciones Condiciones de uso A @@ -462,6 +465,11 @@ Enviar activos a otras redes resultará en una pérdida permanente. Red de %s Envíe fondos utilizando solo + Las mejores oportunidades + Todas las redes + Todos los tipos + Mayormente usado + Ganar Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera @@ -561,6 +569,8 @@ Comentarios en Tangem No se puede completar una transacción Error de descripción de moneda + Fondos insuficientes + Tarifa de transacción Ocurrió un error Ocurrió un error. Código: %s Requiere memo @@ -610,6 +620,7 @@ Finalizar la copia de seguridad primero Incompleto Otros métodos + Guarde su frase de recuperación en un lugar seguro y manténgala privada para proteger sus fondos y configure un código de acceso para mayor seguridad. Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos. Frase de recuperación Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad. @@ -995,6 +1006,7 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware. Todas las ofertas Disponible con %s La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos. @@ -1266,6 +1278,7 @@ La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red. La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red. APR %1$s%% + Sus recompensas de staking comenzarán después de 5 épocas (~25 días) mientras su delegación esté registrada y contabilizada por la red. Reclamar lo que no está en staking Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. @@ -1276,6 +1289,7 @@ APR APY Las recompensas se acumulan automáticamente en su saldo de staking diariamente. + Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s Disponible Tasa de recompensa promedio ¿Cómo funciona el staking? @@ -1420,6 +1434,7 @@ Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques Simplemente cómodo Intercambio a través del proveedor + Sus activos La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. El importe incluye:\n- comisión del proveedor de servicios\n- comisión de red por el envío de %1$s desde el exchange de vuelta a la dirección del usuario. \n\nEl deslizamiento (slippage) del proveedor es de hasta %2$s El importe incluye los honorarios del proveedor de servicios. @@ -1434,6 +1449,7 @@ Fondos insuficientes Dar autorización Intercambiar + Intercambiando... Usted recibe Elige token no disponible @@ -1452,6 +1468,9 @@ Mantén tu dinero seguro. Puedes desbloquear en cualquier momento. Tu tarjeta está congelada. Obtener ayuda + Razón: %s + %s · %s + MCC %s Otro No se puede usar en dispositivos rooteados Completado @@ -1528,10 +1547,15 @@ ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad + El proceso KYC ha fallado KYC en curso Ver estado KYC en progreso para Tangem Pay Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas. + KYC rechazado + Ocultar el bloque KYC + Lo sentimos, no pudimos verificarle + su perfil. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1818,7 +1842,17 @@ Entendido ¡Realmente genial! Actualizar + Iniciar migración + Copiar + La firma de mensajes no es compatible con esta red + No se puede firmar el mensaje. Por favor, inténtelo de nuevo. Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. + Mensaje + Abrir el portal de reclamaciones + Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. + Migración de la red Clore + Firmar + Firma Migración de la red Clore Actualmente estás en el modo Demo Modo demo activo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index f0253cda22..9e7bbf5db8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -43,6 +43,7 @@ Ajouter un compte Enregistrer Nom du compte + Un compte portant ce nom existe déjà. Veuillez choisir un autre nom. Nom de compte déjà utilisé Compte Nouveau compte @@ -298,6 +299,7 @@ Aller au jeton Compris Cacher + Maintenir contre %s heure Importez En cours @@ -322,6 +324,7 @@ Non Aucune adresse Non ajouté + Indisponible Pas maintenant Maintenant OK @@ -372,6 +375,7 @@ Échanger Tangem Tangem Wallet + Appuyez et maintenez enfoncé termes et conditions Conditions d\'utilisation À @@ -399,6 +403,8 @@ Mode Rendement Adresse du contrat copiée ! Réseaux disponibles + La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte. + La dérivation appartient à un autre compte. Jeton ajouté au compte %1$s Adresse du contrat L\'adresse du contrat n\'est pas valide @@ -459,6 +465,11 @@ L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive. %s réseau Envoyez des fonds en utilisant uniquement + Meilleures opportunités + Tous les réseaux + Tous les types + Principalement utilisé + Gagner Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille @@ -558,6 +569,8 @@ Commentaires sur Tangem Impossible d\'effectuer une transaction Erreur de description de la pièce + Fonds insuffisants + Frais de transaction Une erreur s\'est produite Une erreur s\'est produite. Code:%s Un mémo est requis @@ -607,6 +620,7 @@ Finalisez d\'abord la sauvegarde. Incomplet Autres méthodes + Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds, puis configurez un code d\'accès pour plus de sécurité. Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds. Seed phrase Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde. @@ -644,7 +658,7 @@ Une erreur s\'est produite pendant l\'opération. Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus. Accès aux fonds - Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession. + Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seed phrase restera en votre possession. Sécurité générale Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem. Migration des clés @@ -992,6 +1006,7 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel. Toutes les offres Disponible avec %s L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions. @@ -1036,6 +1051,7 @@ Disponible auprès de Disponible jusqu\'à Vous obtenez + Le service est fourni par un prestataire externe. Tangem n\'est pas responsable. Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton Jusqu\'à Via @@ -1248,18 +1264,21 @@ Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Facile à utiliser Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire. - Pas de seedphrase + Pas de seed phrase Le meilleur hardware wallet Tangem Hardware Wallet Nom Faites travailler votre jeton pour vous + Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain. Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking. + Activation du compte Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer. Frais de réseau mis à jour Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. TAEG %1$s%% + Vos récompenses de staking commenceront après 5 époques (~25 jours), une fois que votre délégation aura été enregistrée et comptabilisée par le réseau. Réclamation déstakée Frais de staking du compte Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé. @@ -1270,6 +1289,7 @@ APR APY Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement. + Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s Disponible Taux de récompense moyen Qu\'est-ce que le Staking ? @@ -1325,6 +1345,7 @@ L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker. Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses. Pour commencer le staking, vous devez d’abord activer votre compte TON + Activation du compte Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. 0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. @@ -1413,6 +1434,7 @@ Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics Plus Simple Que Jamais Échange via le fournisseur + Vos actifs Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s Le montant comprend les frais du fournisseur de services. @@ -1427,6 +1449,7 @@ Fonds insuffisants Donner l\'autorisation Échanger + Échange... Vous recevez Choisir le jeton non disponible @@ -1445,6 +1468,9 @@ Geler Votre carte est gelée. Obtenir de l\'aide + Raison : %s + %s · %s + MCC %s Autre Impossible à utiliser sur les appareils rootés Terminé @@ -1521,10 +1547,15 @@ Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment. Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support. Malheureusement, nous n\'avons pas pu vérifier votre identité + KYC a échoué KYC en cours Voir le statut KYC en cours pour Tangem Pay Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures. + KYC rejeté + Masquer le bloc KYC + Désolé, nous n\'avons pas pu vérifier. + votre profil. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte @@ -1579,6 +1610,7 @@ Jeton dans le %%image%% %1$s réseau Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste Impossible de masquer %s + Afficher le code QR Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février. Échangez avec Changelly, %s frais Échangez maintenant @@ -1710,6 +1742,12 @@ Scannez votre carte pour déverrouiller l\'accès Déverrouillage nécessaire Choisissez comment ajouter votre portefeuille + Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille. + Créer un Hardware Wallet + Vous souhaitez acheter un portefeuille Tangem ? + Importer la seed phrase + Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem. + Que choisir ? La blockchain n\'est pas accessible. Réessayez plus tard Scanner la carte ou la bague Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré. @@ -1766,9 +1804,9 @@ Ajouter un portefeuille existant Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité. Scanner un Tangem Wallet - Importez un portefeuille existant à l\'aide de votre seedphrase. + Importez un portefeuille existant à l\'aide de votre seed phrase. Importer un wallet - Entrez la seedphrase + Entrez la seed phrase Vous avez importé votre portefeuille avec succès. Importer un wallet Importation terminée @@ -1984,7 +2022,7 @@ Livraison rapide Commencez en un seul clic Sans faille et sécurisé - Pas de seedphrase + Pas de seed phrase Facile à utiliser Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort. Créer ou importer un portefeuille logiciel @@ -2105,4 +2143,5 @@ Impossible de couvrir les frais %s Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard. Le mode Rendement est indisponible + Impossible de charger le graphique... diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index cae90804ea..bbdf1ef746 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -294,6 +294,7 @@ トークンへ移動 わかりました 非表示 + %sまで長押し 時間 インポート 進行中 @@ -368,6 +369,7 @@ スワップ Tangem Tangem Wallet + タップして長押し 利用規約 利用規約 宛先 @@ -456,6 +458,11 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + おすすめ + すべてのネットワーク + すべての種類 + よく使われています + 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 @@ -935,11 +942,11 @@ 生体認証 シードフレーズについてもっと読む - これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。 + 以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。 あなたのシードフレーズ - %d 単語 + %d個の単語 ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する @@ -1252,6 +1259,7 @@ ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 APR %1$s%% + デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後(約25日後)から開始されます。 ステーキング解除分を請求する ステーキングアカウント手数料 ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 @@ -1441,6 +1449,9 @@ 一時停止 カードが凍結されています サポートを受ける + 理由:%s + %s・%s + MCC %s その他 Root化された端末では使用できません 完了 @@ -1643,25 +1654,69 @@ ウォレット名の変更 すべてのロックを解除 %sですべてをロック解除 + AML認証済み + 利用可能 + ブロック中 + 負債 + 利用上限 + その他(OTPなし) + 単一の取引 + 合計 %d日間利用可能 + 利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。 + %1$sまで利用可能 残高と限度額 + 支払いアカウントの情報が見つかりません。サポートにお問い合わせください。 + トークンの情報を読み込めません。 + 情報を読み込み中です。しばらくお待ちください。 + コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。 アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。 アクセスコード + アカウント有効化 + 登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。 + 有効化をキャンセル 本当に終了してもよろしいですか?中断したところから後で続行できます。 長くはかかりません。アカウントを設定しています。 長くはかかりません。アクティベーションを完了しています。 準備完了です! + その他のウォレット 4桁のコードを設定します。 \nお支払いの際に使用されます。 + PINコード PINコードを作成する PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください + 準備完了です! + Tangemカードを用意し、タップして承認してください。 + Tangemウォレットを準備してください。 + \nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。 ウェブサイトに移動 + ウォレット接続 + ウォレットを選択 + 有効化を続行 アカウントの設定を続けましょう。 お帰りなさい! + 有効化を開始 手順に従ってアカウントを設定してください。 ようこそ! + ブロックチェーン上の金額 + 通貨コード + 日付 + エラーコード + 取引詳細 + 加盟店カテゴリーコード + 加盟店の所在地 + 加盟店の国コード + 加盟店名 + リクエストID + ステータス + 取引 + 取引金額 + トランザクションハッシュ + 取引リクエスト + 取引ステータス + 種類 この取引に異議を唱える ロック解除 カードをスキャンしてアクセスロックを解除する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 45f190c03a..2e53a41bee 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1800,7 +1800,17 @@ Понятно! Очень круто! Обновить + Начать миграцию + Копировать + Подписание сообщений не поддерживается в этой сети + Невозможно подписать сообщение. Пожалуйста, попробуй позже. Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями. + Сообщение + Открыть портал клейма + Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore. + Миграция сети Clore + Подписать + Подпись Миграция сети Clore Вы находитесь в режиме демо Демо режим включен diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 61ee270e13..8ff96286b1 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -44,6 +44,7 @@ Зберегти Назва акаунту Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву. + Обліковий запис з таким ім’ям уже існує Акаунт Новий акаунт Додати акаунт diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 900691f421..222d92855d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1469,6 +1469,9 @@ Freeze Your card is frozen. Get Help + Reason: %s + %s · %s + MCC %s Other Unable to use on rooted devices Completed @@ -1840,7 +1843,17 @@ Ok, Got it! Really cool! Refresh + Start migration + Copy + Message signing is not supported for this network + Unable to sign message. Please try again. According to Clore’s official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned. + Message + Open Claim portal + To keep access to your funds, start the migration according to the official Clore guidelines. + Clore Network Migration + Sign + Signature Clore Network Migration You are currently in the Demo mode Demo mode active diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt index e3343ec90b..ca8dbfd9f9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable sealed class NavigationAction { data object Staking : NavigationAction() data class YieldSupply(val isActive: Boolean) : NavigationAction() + data object CloreMigration : NavigationAction() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt new file mode 100644 index 0000000000..a87418223c --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +sealed class SignCloreMessageError { + + data object WalletManagerNotFound : SignCloreMessageError() + + data object MessageSigningNotSupported : SignCloreMessageError() + + data class SigningFailed(val message: String) : SignCloreMessageError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt new file mode 100644 index 0000000000..c4e4913822 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt @@ -0,0 +1,58 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.MessageSigner +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.common.CompletionResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.SignCloreMessageError +import com.tangem.domain.walletmanager.WalletManagersFacade + +class SignCloreMessageUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + currency: CryptoCurrency, + message: String, + ): Either { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = currency.network, + ) + + if (walletManager == null) { + return SignCloreMessageError.WalletManagerNotFound.left() + } + + if (walletManager !is MessageSigner) { + return SignCloreMessageError.MessageSigningNotSupported.left() + } + + val signer = when (userWallet) { + is UserWallet.Cold -> { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true + cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = null, + ) + } + is UserWallet.Hot -> getHotWalletSigner(userWallet) + } + + return when (val result = walletManager.signMessage(message, signer)) { + is CompletionResult.Success -> result.data.right() + is CompletionResult.Failure -> SignCloreMessageError.SigningFailed( + message = result.error.message ?: "Unknown error", + ).left() + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 05ec40ad40..10cb869ad1 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { /** Core modules */ implementation(projects.common.routing) implementation(projects.core.navigation) + implementation(projects.core.res) implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.analytics) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt new file mode 100644 index 0000000000..51e1125b6b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt @@ -0,0 +1,113 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.model + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.SignCloreMessageError +import com.tangem.domain.transaction.usecase.SignCloreMessageUseCase +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) +@Suppress("LongParameterList") +internal class CloreMigrationModel( + private val stateFactory: TokenDetailsStateFactory, + private val signCloreMessageUseCase: SignCloreMessageUseCase, + private val clipboardManager: ClipboardManager, + private val uiMessageSender: UiMessageSender, + private val router: InnerTokenDetailsRouter, + private val userWallet: UserWallet, + private val cryptoCurrency: CryptoCurrency, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, + private val onStateUpdate: (TokenDetailsState) -> Unit, +) { + + private val state = MutableStateFlow(CloreMigrationState()) + + fun onCloreMigrationClick() { + state.value = CloreMigrationState() + updateBottomSheet() + } + + fun onCloreSignMessage(message: String) { + if (message.isBlank()) return + + state.value = state.value.copy(message = message) + updateBottomSheet(isSigningInProgress = true) + + coroutineScope.launch(dispatchers.main) { + signCloreMessageUseCase( + userWallet = userWallet, + currency = cryptoCurrency, + message = message, + ).fold( + ifLeft = { error -> + val errorMessage = when (error) { + is SignCloreMessageError.SigningFailed -> stringReference(error.message) + SignCloreMessageError.MessageSigningNotSupported -> + resourceReference(R.string.warning_clore_migration_error_signing_not_supported) + SignCloreMessageError.WalletManagerNotFound -> + resourceReference(R.string.warning_clore_migration_error_wallet_manager_not_found) + } + state.value = state.value.copy(signature = "") + updateBottomSheet() + uiMessageSender.send(SnackbarMessage(errorMessage)) + }, + ifRight = { signature -> + state.value = state.value.copy(signature = signature) + onStateUpdate(stateFactory.getStateWithUpdatedCloreMigrationSignature(signature)) + }, + ) + } + } + + fun onOpenCloreClaimPortal() { + router.openUrl(CLAIM_PORTAL_URL) + } + + private fun updateBottomSheet(isSigningInProgress: Boolean = false) { + val currentState = state.value + onStateUpdate( + stateFactory.getStateWithCloreMigrationBottomSheet( + message = currentState.message, + signature = currentState.signature, + isSigningInProgress = isSigningInProgress, + onMessageChange = { newMessage -> + state.value = state.value.copy(message = newMessage) + updateBottomSheet() + }, + onSignClick = { onCloreSignMessage(state.value.message) }, + onCopyClick = { + val signature = state.value.signature + if (signature.isNotBlank()) { + clipboardManager.setText(text = signature, isSensitive = false) + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)), + ) + } + }, + onOpenPortalClick = { onOpenCloreClaimPortal() }, + ), + ) + } + + private companion object { + const val CLAIM_PORTAL_URL = "https://claim-portal.clore.ai/" + } +} + +private data class CloreMigrationState( + val message: String = "", + val signature: String = "", +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index 66d495e481..b98369cf0c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -72,6 +72,19 @@ interface TokenDetailsClickIntents { fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) + fun onYieldInfoClick() + + // region Clore migration + // TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY]) + + fun onCloreMigrationClick() + + fun onCloreSignMessage(message: String) + + fun onOpenCloreClaimPortal() + + // endregion Clore migration + fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) fun onOpenUrlClick(url: String) @@ -79,6 +92,4 @@ interface TokenDetailsClickIntents { fun onConfirmDisposeExpressStatus() fun onDisposeExpressStatus() - - fun onYieldInfoClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f1e1c2c815..91134bfd30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -155,13 +155,17 @@ internal class TokenDetailsModel @Inject constructor( private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, -) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback { + private val signCloreMessageUseCase: SignCloreMessageUseCase, +) : Model(), + TokenDetailsClickIntents, + YieldSupplyDepositedWarningComponent.ModelCallback { private val params = paramsContainer.require() private val userWalletId: UserWalletId = params.userWalletId private val cryptoCurrency: CryptoCurrency = params.currency - private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found") + private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() + ?: error("UserWallet not found") private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() @@ -193,6 +197,27 @@ internal class TokenDetailsModel @Inject constructor( yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) + private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) + val uiState: StateFlow = internalUiState + + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) { + CloreMigrationModel( + stateFactory = stateFactory, + signCloreMessageUseCase = signCloreMessageUseCase, + clipboardManager = clipboardManager, + uiMessageSender = uiMessageSender, + router = router, + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + coroutineScope = modelScope, + dispatchers = dispatchers, + onStateUpdate = { internalUiState.value = it }, + ) + } + // endregion + private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { expressStatusFactory.create( clickIntents = this, @@ -215,9 +240,6 @@ internal class TokenDetailsModel @Inject constructor( TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler) } - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - val uiState: StateFlow = internalUiState - init { updateTopBarMenu() initButtons() @@ -666,9 +688,8 @@ internal class TokenDetailsModel @Inject constructor( vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) clipboardManager.setText(text = extendedKey, isSensitive = true) - uiMessageSender.send( - message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)), - ) + val message = resourceReference(R.string.wallet_notification_address_copied) + uiMessageSender.send(message = SnackbarMessage(message = message)) } } } @@ -785,7 +806,8 @@ internal class TokenDetailsModel @Inject constructor( modelScope.launch(dispatchers.main) { when (val addresses = currencyStatus.value.networkAddress) { is NetworkAddress.Selectable -> { - internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) + internalUiState.value = + stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) } is NetworkAddress.Single -> { router.openUrl( @@ -1265,11 +1287,26 @@ internal class TokenDetailsModel @Inject constructor( } private fun handleNavigationParam() { - if (params.navigationAction is NavigationAction.Staking) { - openStaking() + when (params.navigationAction) { + is NavigationAction.Staking -> openStaking() + is NavigationAction.CloreMigration -> onCloreMigrationClick() + is NavigationAction.YieldSupply, + null, + -> Unit } } + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + + override fun onCloreMigrationClick() = cloreMigrationModel.onCloreMigrationClick() + + override fun onCloreSignMessage(message: String) = cloreMigrationModel.onCloreSignMessage(message) + + override fun onOpenCloreClaimPortal() = cloreMigrationModel.onOpenCloreClaimPortal() + + // endregion Clore migration + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 5733db0e18..b8a992221f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -258,9 +258,15 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.warning_matic_migration_message), ) - data object MigrationClore : Warning( + data class MigrationClore( + private val onMigrationClick: () -> Unit, + ) : Warning( title = resourceReference(id = R.string.warning_clore_migration_title), - subtitle = resourceReference(id = R.string.warning_clore_migration_message), + subtitle = resourceReference(id = R.string.warning_clore_migration_sheet_description), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.warning_clore_migration_button), + onClick = onMigrationClick, + ), ) data object UsedOutdatedData : TokenDetailsNotification( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index eb54aca878..38588b2f69 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -155,7 +155,9 @@ internal class TokenDetailsNotificationConverter( }, ) is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol - is CryptoCurrencyWarning.MigrationClore -> MigrationClore + is CryptoCurrencyWarning.MigrationClore -> MigrationClore( + onMigrationClick = clickIntents::onCloreMigrationClick, + ) is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index da13ce39ba..953f1376e8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -378,4 +379,62 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } + + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + + fun getStateWithCloreMigrationBottomSheet( + message: String, + signature: String, + isSigningInProgress: Boolean, + onMessageChange: (String) -> Unit, + onSignClick: () -> Unit, + onCopyClick: () -> Unit, + onOpenPortalClick: () -> Unit, + ): TokenDetailsState { + return currentStateProvider().copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = CloreMigrationBottomSheetConfig( + message = message, + signature = signature, + isSigningInProgress = isSigningInProgress, + onMessageChange = onMessageChange, + onSignClick = onSignClick, + onCopyClick = onCopyClick, + onOpenPortalClick = onOpenPortalClick, + ), + ), + ) + } + + fun getStateWithUpdatedCloreMigrationSignature(signature: String): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig ?: return state + val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state + + return state.copy( + bottomSheetConfig = bottomSheetConfig.copy( + content = content.copy( + signature = signature, + isSigningInProgress = false, + ), + ), + ) + } + + fun getStateWithCloreMigrationSigning(): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig ?: return state + val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state + + return state.copy( + bottomSheetConfig = bottomSheetConfig.copy( + content = content.copy(isSigningInProgress = true), + ), + ) + } + + // endregion Clore migration } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index f3466d9025..4dc151587f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -35,6 +35,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock import com.tangem.features.markets.token.block.TokenMarketBlockComponent @@ -45,7 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow // TODO: Split to blocks [REDACTED_JIRA] -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable internal fun TokenDetailsScreen( state: TokenDetailsState, @@ -164,17 +166,25 @@ internal fun TokenDetailsScreen( TokenDetailsDialogs(state = state) - state.bottomSheetConfig?.let { config -> - when (config.content) { - is TokenReceiveBottomSheetConfig -> { - TokenReceiveBottomSheet(config = config) - } - is ChooseAddressBottomSheetConfig -> { - ChooseAddressBottomSheet(config = config) - } - is ExpressStatusBottomSheetConfig -> { - ExpressStatusBottomSheet(config = config) - } + TokenDetailsBottomSheets(state = state) + } +} + +@Composable +private fun TokenDetailsBottomSheets(state: TokenDetailsState) { + state.bottomSheetConfig?.let { config -> + when (config.content) { + is TokenReceiveBottomSheetConfig -> { + TokenReceiveBottomSheet(config = config) + } + is ChooseAddressBottomSheetConfig -> { + ChooseAddressBottomSheet(config = config) + } + is ExpressStatusBottomSheetConfig -> { + ExpressStatusBottomSheet(config = config) + } + is CloreMigrationBottomSheetConfig -> { + CloreMigrationBottomSheet(config = config) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt new file mode 100644 index 0000000000..9875449e80 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt @@ -0,0 +1,220 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore + +import android.content.res.Configuration +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R as CoreR +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun CloreMigrationBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + title = { + TangemModalBottomSheetTitle( + endIconRes = CoreR.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + }, + ) { content: CloreMigrationBottomSheetConfig -> + CloreMigrationBottomSheetContent(content = content) + } +} + +@Composable +private fun CloreMigrationBottomSheetContent(content: CloreMigrationBottomSheetConfig) { + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + ) { + Text( + text = stringResourceSafe(R.string.warning_clore_migration_sheet_title), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + Text( + text = stringResourceSafe(R.string.warning_clore_migration_sheet_description), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body2, + modifier = Modifier.padding(horizontal = 8.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MessageField( + placeholder = stringResourceSafe(R.string.warning_clore_migration_message_label), + value = content.message, + onValueChange = content.onMessageChange, + buttonText = stringResourceSafe(R.string.warning_clore_migration_sign_button), + onButtonClick = content.onSignClick, + isButtonEnabled = content.message.isNotBlank() && !content.isSigningInProgress, + isLoading = content.isSigningInProgress, + ) + + SignatureField( + placeholder = stringResourceSafe(R.string.warning_clore_migration_signature_label), + value = content.signature, + onCopyClick = content.onCopyClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PrimaryButton( + text = stringResourceSafe(R.string.warning_clore_migration_open_portal_button), + onClick = content.onOpenPortalClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun MessageField( + placeholder: String, + value: String, + onValueChange: (String) -> Unit, + buttonText: String, + onButtonClick: () -> Unit, + isButtonEnabled: Boolean, + isLoading: Boolean, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = TangemTheme.shapes.roundedCornersSmall, + ) + .heightIn(min = 56.dp) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SimpleTextField( + value = value, + onValueChange = onValueChange, + placeholder = TextReference.Str(placeholder), + modifier = Modifier.weight(1f), + singleLine = false, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Str(buttonText), + onClick = onButtonClick, + isEnabled = isButtonEnabled, + isLoading = isLoading, + ), + ) + } +} + +@Suppress("FunctionSignature") +@Composable +private fun SignatureField(placeholder: String, value: String, onCopyClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = TangemTheme.shapes.roundedCornersSmall, + ) + .heightIn(min = 56.dp) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SimpleTextField( + value = value, + onValueChange = {}, + placeholder = TextReference.Str(placeholder), + modifier = Modifier.weight(1f), + readOnly = true, + singleLine = false, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.warning_clore_migration_copy_button), + onClick = onCopyClick, + isEnabled = value.isNotBlank(), + ), + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CloreMigrationBottomSheet() { + TangemThemePreview { + val config = TangemBottomSheetConfig( + isShown = true, + content = CloreMigrationBottomSheetConfig( + message = "Claim request for CLORE tokens to Ethereum address " + + "0x742d35Cc6634C0532925a3b844Bc9e7595f60126 from CEsMERNgBVPo9Dkc99pRDMPD3mxwPMxHZi", + signature = "H4sIAAAAAAAEAGNgGAWjYBSMglEwCkYBHQEA", + isSigningInProgress = false, + onMessageChange = {}, + onSignClick = {}, + onCopyClick = {}, + onOpenPortalClick = {}, + ), + onDismissRequest = {}, + ) + + CloreMigrationBottomSheet(config) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +private fun Preview_CloreMigrationBottomSheet_Empty() { + TangemThemePreview { + val config = TangemBottomSheetConfig( + isShown = true, + content = CloreMigrationBottomSheetConfig( + message = "", + signature = "", + isSigningInProgress = false, + onMessageChange = {}, + onSignClick = {}, + onCopyClick = {}, + onOpenPortalClick = {}, + ), + onDismissRequest = {}, + ) + + CloreMigrationBottomSheet(config) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt new file mode 100644 index 0000000000..9adf70e4af --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +@Immutable +data class CloreMigrationBottomSheetConfig( + val message: String = "", + val signature: String = "", + val isSigningInProgress: Boolean = false, + val onMessageChange: (String) -> Unit, + val onSignClick: () -> Unit, + val onCopyClick: () -> Unit, + val onOpenPortalClick: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index d301a20f0c..2526056e5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -202,6 +202,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( navigationAction = navigationAction, apy = apy, ) + is NavigationAction.CloreMigration -> Unit } } @@ -298,6 +299,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( }, ) } + is NavigationAction.CloreMigration -> return } analyticsEventHandler.send(event) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ed19a09083..2f90546c14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -18,6 +18,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -35,6 +36,7 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked +import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.usecase.* @@ -65,6 +67,8 @@ internal interface WalletWarningsClickIntents { fun onAddBackupCardClick() + fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCloseAlreadySignedHashesWarningClick() fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) @@ -146,6 +150,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( prepareAndStartOnboardingProcess() } + override fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedUserWallet() ?: return + + router.openTokenDetails( + userWalletId = userWallet.walletId, + currencyStatus = cryptoCurrencyStatus, + navigationAction = NavigationAction.CloreMigration, + ) + } + private fun prepareAndStartOnboardingProcess() { modelScope.launch(dispatchers.main) { getSelectedUserWallet()?.requireColdWallet()?.let { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 94ede58c14..62dc30de88 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -76,6 +76,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UsedOutdatedData, is WalletNotification.UnlockVisaAccess, is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event + is WalletNotification.CloreMigration, -> null is WalletNotification.FinishWalletActivation -> { val activationState = if (warning.isBackupExists) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index b757338349..ed279ff217 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -33,6 +33,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.isPositive import com.tangem.utils.extensions.orZero @@ -317,6 +318,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( element = WalletNotification.Warning.SomeNetworksUnreachable, condition = flattenCurrencies.hasUnreachableNetworks(), ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: Lce>, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotification.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { + val currencies = getOrNull(isPartialContentAccepted = true) ?: return null + + return currencies.find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } } private fun MutableList.addPushReminderNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e2dc6648fc..b0563ac622 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -414,4 +414,18 @@ sealed class WalletNotification(val config: NotificationConfig) { iconSize = 54.dp, ), ) + + data class CloreMigration( + val onStartMigrationClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_sheet_description), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), + onClick = onStartMigrationClick, + ), + ), + ) } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d8e493fcf3..d7281ac75f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1388" +tangemBlockchainSdk = "releases-5.32-1404" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.32-574" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/tangem-android-tools b/tangem-android-tools index 28079b094a..cfc2c0ab47 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 28079b094a4518de068161226576747089519918 +Subproject commit cfc2c0ab47e2e821f38f463f32f120d620804c72 From 9d036548f8737eb1933910932726c612fec99e73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 13:03:21 +0300 Subject: [PATCH 04/28] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d811278a2d..9f554a7f91 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1398" +tangemBlockchainSdk = "releases-5.33-1406" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.33-576" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 3d70278af1f0a0c276878f77160d595bc9da11c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 14:20:25 +0400 Subject: [PATCH 05/28] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 3 ++- .../tokendetails/state/components/TokenDetailsNotification.kt | 2 +- .../presentation/wallet/state/model/WalletNotification.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 222d92855d..39efffd2f0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1845,12 +1845,13 @@ Refresh Start migration Copy + To keep access to your funds, start the migration according to the official Clore guidelines. Message signing is not supported for this network Unable to sign message. Please try again. According to Clore’s official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned. Message Open Claim portal - To keep access to your funds, start the migration according to the official Clore guidelines. + To continue using your Clore tokens, you must complete the token migration according to the information on the Claim Portal. Clore Network Migration Sign Signature diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index b8a992221f..feb6279306 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -262,7 +262,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { private val onMigrationClick: () -> Unit, ) : Warning( title = resourceReference(id = R.string.warning_clore_migration_title), - subtitle = resourceReference(id = R.string.warning_clore_migration_sheet_description), + subtitle = resourceReference(id = R.string.warning_clore_migration_description), buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(id = R.string.warning_clore_migration_button), onClick = onMigrationClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index b0563ac622..5d67cf05e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -420,7 +420,7 @@ sealed class WalletNotification(val config: NotificationConfig) { ) : WalletNotification( config = NotificationConfig( title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), - subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_sheet_description), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description), iconResId = R.drawable.img_attention_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), From 032fdee5ec5c6e989c5fdad653d992a55c2b79fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 13:27:48 +0300 Subject: [PATCH 06/28] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- tangem-android-tools | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8285f8d289..b55ca9ec9b 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304 +Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125 diff --git a/tangem-android-tools b/tangem-android-tools index ab6aee1d49..dc9df17919 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit ab6aee1d49ac0cfca644330a525dced725d5a257 +Subproject commit dc9df17919126bce93482559fe26602f9801eb42 From d6f6af79e69c12a48a5002eaa3e49074bef91afd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 15:55:46 +0300 Subject: [PATCH 07/28] Updated on 2026-08-14 --- .../core/ui/utils/SharedTransitionUtils.kt | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt index 4851e2e34f..801c42088c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt @@ -1,5 +1,9 @@ package com.tangem.core.ui.utils +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.Box @@ -7,6 +11,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection @Composable fun TangemSharedTransitionLayout( @@ -31,6 +42,78 @@ fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Compos } } -private val LocalSharedTransitionScope = staticCompositionLocalOf { - error("No SharedTransitionScope provided") +private val LocalSharedTransitionScope = staticCompositionLocalOf { StubSharedTransitionScope() } + +private class StubSharedTransitionScope : SharedTransitionScope { + override val isTransitionActive: Boolean = false + + override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates + get() = error("No LookaheadScopeCoordinates available in StubSharedTransitionScope") + + override fun Modifier.skipToLookaheadSize(enabled: () -> Boolean): Modifier { + return this + } + + override fun Modifier.renderInSharedTransitionScopeOverlay( + zIndexInOverlay: Float, + renderInOverlay: () -> Boolean, + ): Modifier { + return this + } + + override fun Modifier.sharedElement( + sharedContentState: SharedTransitionScope.SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + boundsTransform: BoundsTransform, + placeholderSize: SharedTransitionScope.PlaceholderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ): Modifier { + return this + } + + override fun Modifier.sharedBounds( + sharedContentState: SharedTransitionScope.SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + enter: EnterTransition, + exit: ExitTransition, + boundsTransform: BoundsTransform, + resizeMode: SharedTransitionScope.ResizeMode, + placeholderSize: SharedTransitionScope.PlaceholderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ): Modifier { + return this + } + + override fun Modifier.sharedElementWithCallerManagedVisibility( + sharedContentState: SharedTransitionScope.SharedContentState, + visible: Boolean, + boundsTransform: BoundsTransform, + placeholderSize: SharedTransitionScope.PlaceholderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ): Modifier { + return this + } + + override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip { + return object : SharedTransitionScope.OverlayClip { + override fun getClipPath( + sharedContentState: SharedTransitionScope.SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density, + ): Path? { + return Path() + } + } + } + + override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates { + return this + } } \ No newline at end of file From cea2e103952233348b2050dc1e89b6397a6be26a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 16:26:26 +0100 Subject: [PATCH 08/28] Updated on 2026-08-14 --- .../com/tangem/common/ui/news/ArticleCard.kt | 127 ++++++++--------- .../java/com/tangem/common/ui/news/Tags.kt | 128 ++++++++++++++++++ .../res/drawable/bg_trending_article.webp | Bin 0 -> 23452 bytes .../tangem/features/feed/ui/feed/FeedList.kt | 2 +- 4 files changed, 182 insertions(+), 75 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt create mode 100644 core/ui/src/main/res/drawable/bg_trending_article.webp diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 43f01a3e93..a6a7a2a078 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -2,7 +2,10 @@ package com.tangem.common.ui.news import android.content.res.Configuration import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardColors import androidx.compose.material3.Text @@ -11,6 +14,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -21,15 +26,12 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.utils.StringsSigns -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @@ -65,52 +67,61 @@ private fun TrendingArticle( colors: CardColors, modifier: Modifier = Modifier, ) { - Column( + Box( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(colors.containerColor) - .clickable { onArticleClick() } - .padding(vertical = 24.dp, horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .clickable { onArticleClick() }, ) { - Text( - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - text = stringResourceSafe(R.string.feed_trending_now), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.icon.accent, + Image( + modifier = Modifier.matchParentSize(), + painter = painterResource(R.drawable.bg_trending_article), + contentDescription = null, + contentScale = ContentScale.FillWidth, ) + Column( + modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + text = stringResourceSafe(R.string.feed_trending_now), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + ) - SpacerH(12.dp) + SpacerH(12.dp) - Text( - text = articleConfigUM.title, - color = if (articleConfigUM.isViewed) { - TangemTheme.colors.text.tertiary - } else { - TangemTheme.colors.text.primary1 - }, - style = TangemTheme.typography.h3, - textAlign = TextAlign.Center, - ) + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors.text.tertiary + } else { + TangemTheme.colors.text.primary1 + }, + style = TangemTheme.typography.h3, + textAlign = TextAlign.Center, + ) - SpacerH(8.dp) + SpacerH(8.dp) - ArticleInfo( - score = articleConfigUM.score, - createdAt = articleConfigUM.createdAt.resolveReference(), - ) + ArticleInfo( + score = articleConfigUM.score, + createdAt = articleConfigUM.createdAt.resolveReference(), + ) - SpacerH(32.dp) + SpacerH(32.dp) - Tags( - modifier = Modifier.padding(horizontal = 46.dp), - tags = articleConfigUM.tags.toImmutableList(), - ) + Tags( + modifier = Modifier.padding(horizontal = 46.dp), + tags = articleConfigUM.tags.toImmutableList(), + ) + } } } @@ -182,46 +193,14 @@ private fun DefaultArticle( overflow = TextOverflow.Ellipsis, ) - SpacerHMax(modifier = Modifier.padding(bottom = 16.dp)) + SpacerH(16.dp) + + SpacerHMax() Tags(tags = articleConfigUM.tags.toImmutableList()) } } -@Composable -private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { - FlowRow( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1, - maxLines = 1, - itemVerticalAlignment = Alignment.Bottom, - ) { - val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) { - tags.take(MAX_TAGS_COUNT_TO_SHOW) - } else { - tags - } - - displayedTags.forEach { tag -> - Label(state = tag) - } - - if (tags.size > MAX_TAGS_COUNT_TO_SHOW) { - val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW - Label( - state = LabelUM( - text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"), - maxLines = 1, - ), - ) - } - } -} - -private const val MAX_TAGS_COUNT_TO_SHOW = 3 - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt new file mode 100644 index 0000000000..a163c8ee15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt @@ -0,0 +1,128 @@ +package com.tangem.common.ui.news + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.layout.SubcomposeMeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.utils.StringsSigns +import kotlinx.collections.immutable.ImmutableList + +private val TAG_SPACING = 4.dp + +/** + * A layout that displays a list of tags, automatically calculating how many can fit + * in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N"). + * + * @param tags The immutable list of [LabelUM] to display. + * @param modifier The modifier to be applied to the layout. + */ +@Composable +internal fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { + if (tags.isEmpty()) return + + SubcomposeLayout(modifier = modifier) { constraints -> + val spacingPx = TAG_SPACING.roundToPx() + + val tagPlaceables = subcompose(ContentSlot.Tags) { + tags.forEach { tag -> + Label(state = tag) + } + }.map { it.measure(constraints) } + + val layoutInfo = calculateLayoutInfo( + maxWidth = constraints.maxWidth, + spacingPx = spacingPx, + tagPlaceables = tagPlaceables, + ) + + val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let { + subcompose(ContentSlot.Overflow) { + OverflowLabel(count = it) + }.first().measure(constraints) + } + + val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount) + val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable) + + val width = calculateRowWidth( + placeables = allVisibleItems, + spacingPx = spacingPx, + ).coerceIn(constraints.minWidth, constraints.maxWidth) + + val height = allVisibleItems.maxOfOrNull { it.height } ?: 0 + + layout(width = width, height = height) { + var xPosition = 0 + allVisibleItems.forEachIndexed { index, placeable -> + if (index > 0) { + xPosition += spacingPx + } + val yPosition = height - placeable.height + placeable.placeRelative(xPosition, yPosition) + xPosition += placeable.width + } + } + } +} + +/** + * Calculates how many tags can be displayed and how many are overflowing. + */ +private fun SubcomposeMeasureScope.calculateLayoutInfo( + maxWidth: Int, + spacingPx: Int, + tagPlaceables: List, +): TagsLayoutInfo { + val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx) + + if (totalWidthWithoutOverflow <= maxWidth) { + return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0) + } + + for (count in tagPlaceables.indices.reversed()) { + val visibleTags = tagPlaceables.take(count) + val overflowCount = tagPlaceables.size - count + + val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") { + OverflowLabel(count = overflowCount) + }.first().measure(Constraints()) + + val widthWithOverflow = calculateRowWidth( + placeables = visibleTags + tempOverflowPlaceable, + spacingPx = spacingPx, + ) + + if (widthWithOverflow <= maxWidth) { + return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount) + } + } + + return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size) +} + +@Composable +private fun OverflowLabel(count: Int) { + Label( + state = LabelUM( + text = TextReference.Str("${StringsSigns.PLUS}$count"), + maxLines = 1, + ), + ) +} + +private fun calculateRowWidth(placeables: List, spacingPx: Int): Int { + if (placeables.isEmpty()) return 0 + val tagsWidth = placeables.sumOf { it.width } + val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0) + return tagsWidth + spacingWidth +} + +private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int) + +private enum class ContentSlot { Tags, Overflow } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/bg_trending_article.webp b/core/ui/src/main/res/drawable/bg_trending_article.webp new file mode 100644 index 0000000000000000000000000000000000000000..42951498d8ff88c566a5e81d56527d863b7d58c7 GIT binary patch literal 23452 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 P%dAQ_jThJe00000uIq=G literal 0 HcmV?d00001 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 88ebc8dc1f..43dcd4a50e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -423,7 +423,7 @@ private fun Charts( ) { BlockCard( modifier = modifier, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary), ) { Column(modifier = Modifier.fillMaxWidth()) { when (marketChart) { From 9bca60feedbc842af3efb297d3348b5b8bbcef61 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 18:31:21 +0200 Subject: [PATCH 09/28] Updated on 2026-08-14 --- .../promotion/models/PromoBannerV2Response.kt | 10 ++++++++++ .../api/tangemTech/TangemTechApi.kt | 7 +++++++ core/res/src/main/res/values-de/strings.xml | 3 +++ core/res/src/main/res/values-es/strings.xml | 3 +++ core/res/src/main/res/values-fr/strings.xml | 5 ++++- core/res/src/main/res/values-ja/strings.xml | 3 +++ core/res/src/main/res/values-ru/strings.xml | 3 +++ .../src/main/res/values-uk-rUA/strings.xml | 3 +++ core/res/src/main/res/values/strings.xml | 5 +++++ .../data/promo/DefaultPromoRepository.kt | 16 +++++++++++++++ .../tangem/domain/promo/models/PromoBanner.kt | 1 + .../promo/ShouldShowPromoWalletUseCase.kt | 1 + .../model/analytics/PromoAnalyticsEvent.kt | 1 + .../intents/WalletWarningsClickIntents.kt | 20 +++++++++++++++++++ .../utils/WalletWarningsAnalyticsSender.kt | 4 ++++ .../domain/GetMultiWalletWarningsFactory.kt | 18 +++++++++++++++++ .../wallet/state/model/WalletNotification.kt | 17 ++++++++++++++++ .../main/res/drawable/ic_yield_promo_36.xml | 15 ++++++++++++++ 18 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt create mode 100644 features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt new file mode 100644 index 0000000000..23b62757cb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.promotion.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PromoBannerV2Response( + @Json(name = "promotions") + val promotions: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index a3b38cb1f0..f9bdbc2aa6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.promotion.models.PromoBannerResponse +import com.tangem.datasource.api.promotion.models.PromoBannerV2Response import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse @@ -188,6 +189,12 @@ interface TangemTechApi { @Query("programName") name: String, @Header("Cache-Control") cacheControl: String = "max-age=600", ): ApiResponse + + @GET("/v2/promotion") + suspend fun getPromoBannersV2( + @Query("walletId") walletId: String, + @Header("Cache-Control") cacheControl: String = "max-age=600", + ): ApiResponse // endregion /** diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 2a9bd1b3eb..4175f06e1c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -899,6 +899,9 @@ Krypto mit SEPA kaufen Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab. Tangem Visa Card + Bedingungen + Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10. + Yield-Mode-Kampagne Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen Schützen Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 124b0453e1..c3b8bb8a6a 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -907,6 +907,9 @@ Comprar criptomonedas con SEPA Únete a la lista de espera y consigue una tarjeta de pago como ninguna otra. Tangem Visa Card + Términos y condiciones + Deposite $100 o más, manténgalo durante 30 días y reciba $10. + ¡Únase a la campaña Yield Mode! Configure un código de acceso único para proteger todos sus dispositivos. Proteger Puede configurar un código de acceso individual en cada tarjeta más adelante diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 9e7bbf5db8..d88cdd4e57 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -907,6 +907,9 @@ Acheter des cryptomonnaies avec SEPA Rejoignez la liste d’attente et obtenez une carte de paiement pas comme les autres. Tangem Visa Card + Conditions générales + Déposez 100 $ ou plus, conservez le dépôt pendant 30 jours et recevez 10 $. + Rejoignez la campagne Yield Mode ! Vous devez définir un seul code d\'accès pour protéger tous vos appareils. Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -1468,7 +1471,7 @@ Geler Votre carte est gelée. Obtenir de l\'aide - Raison : %s + Raison: %s %s · %s MCC %s Autre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index bbdf1ef746..770eb98276 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -894,6 +894,9 @@ SEPAで暗号資産を購入 事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。 Tangem Visaカード + 利用規約 + 100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。 + Yield Mode キャンペーンに参加しよう! すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2e53a41bee..b0e42bdf47 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -925,6 +925,9 @@ Покупайте крипту через SEPA Присоединяйтесь к списку ожидания и получите уникальную платёжную карту, не похожую ни на одну другую. Tangem Visa Card + Условия программы + Внесите депозит от $100, удерживайте его 30 дней и получите бонус $10 + Подключите Режим доходности! Установите единый код доступа для защиты всех ваших карт или колец Защита Установите индивидуальный код доступа для каждой карты или кольца позже. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 8ff96286b1..67360c1ce3 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -909,6 +909,9 @@ Купуйте криптовалюту через SEPA Приєднуйтесь до списку очікування та отримайте унікальну платіжну картку, якої ще не було. Tangem Visa Card + Умови та положення + Внесіть депозит від $100, утримуйте його 30 днів та отримайте $10. + Кампанія Yield Mode! Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець Захист Встановіть індивідуальний код доступу для кожної картки або кільця пізніше. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 39efffd2f0..ed8c132108 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -86,6 +86,8 @@ Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Default Legacy + Try to reset biometrics on your device or contact support + Authentication error How to scan Request support Try again @@ -908,6 +910,9 @@ Buy Crypto with SEPA Join the waitlist and get a payment card unlike any other Tangem Visa Card + Terms and conditions + Deposit $100+, hold for 30 days, get $10 + Join the Yield Mode Campaign! Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 31133bb96f..625fdc2535 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -66,6 +66,11 @@ internal class DefaultPromoRepository( PromoId.OnePlusOne -> { val isActive = getOnePlusOnePromoBanner()?.isActive == true + isActive && shouldShow + } + PromoId.YieldPromo -> { + val isActive = getYieldPromoBanner(userWalletId)?.isActive == true + isActive && shouldShow } } @@ -79,6 +84,7 @@ internal class DefaultPromoRepository( PromoId.VisaPresale -> flowOf(false) PromoId.BlackFriday -> flowOf(false) PromoId.OnePlusOne -> flowOf(false) + PromoId.YieldPromo -> flowOf(false) } } @@ -190,12 +196,22 @@ internal class DefaultPromoRepository( }.getOrNull() } + private suspend fun getYieldPromoBanner(userWalletId: UserWalletId): PromoBanner? { + return runCatching(dispatchers.io) { + val response = tangemApi.getPromoBannersV2(userWalletId.stringValue).getOrThrow() + val yieldPromotion = response.promotions.find { it.name == YIELD_PROMO_NAME } + ?: return@runCatching null + promoBannerConverter.convert(yieldPromotion) + }.getOrNull() + } + private companion object { const val SEPA_NAME = "sepa" const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" const val MOONPAY_NAME = "moonpay" const val ONE_PLUS_ONE_NAME = "one-plus-one" + const val YIELD_PROMO_NAME = "promo-yield" const val STORIES_LOAD_DELAY = 1000L } } \ No newline at end of file diff --git a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt index 9f8055dc60..ee31586b29 100644 --- a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt +++ b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt @@ -31,4 +31,5 @@ enum class PromoId { VisaPresale, BlackFriday, OnePlusOne, + YieldPromo, } \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt index 5cab5b1cde..7f6f41114d 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt @@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase( PromoId.VisaPresale, PromoId.BlackFriday, PromoId.OnePlusOne, + PromoId.YieldPromo, -> true PromoId.Sepa -> { val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt index 3d3f0fbdb1..df7fde5ad2 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt @@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent( Sepa("Sepa"), BlackFriday("Black Friday"), OnePlusOne("One-Plus-One"), + YieldPromo("Yield Promo"), } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 2f90546c14..55dd5e9a5c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -108,6 +108,8 @@ internal interface WalletWarningsClickIntents { fun onDenyPermissions() fun onFinishWalletActivationClick(isBackupExists: Boolean) + + fun onYieldPromoTermsAndConditionsClick() } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -337,6 +339,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( program = Program.OnePlusOne, action = PromotionBannerClicked.BannerAction.Closed(), ) + PromoId.YieldPromo -> PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + action = PromotionBannerClicked.BannerAction.Closed(), + ) }, ) @@ -394,6 +401,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK) } + PromoId.YieldPromo -> Unit // banner is not clickable, only terms and conditions button } } @@ -611,6 +619,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onYieldPromoTermsAndConditionsClick() { + analyticsEventHandler.send( + PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + action = PromotionBannerClicked.BannerAction.Clicked(), + ), + ) + urlOpener.openUrl(YIELD_PROMO_TERMS_LINK) + } + private companion object { const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" + "&utm_medium=banner" + @@ -625,5 +644,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( "&utm_source=tangem-app-banner" + "&utm_medium=banner" + "&utm_campaign=BOGO50" + const val YIELD_PROMO_TERMS_LINK = "https://tangem.com/docs/yield-mode-toc.html" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 62dc30de88..3852ada00e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -66,6 +66,10 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( source = AnalyticsParam.ScreensSources.Main, program = Program.OnePlusOne, ) + is WalletNotification.YieldPromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + ) is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo() is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo() is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index ed279ff217..fe3861e0c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -96,6 +96,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key) .distinctUntilChanged(), getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) + .distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> @@ -108,6 +110,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowOnePlusOnePromo = array[4] as Boolean val shouldShowEnablePushesReminderNotification = array[5] as Boolean val shouldAccessCodeSkipped = array[6] as Boolean + val shouldShowYieldPromo = array[7] as Boolean buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -123,6 +126,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addOnePlusOnePromoNotification(clickIntents, shouldShowOnePlusOnePromo) + addYieldPromoNotification(clickIntents, shouldShowYieldPromo) + addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) @@ -287,6 +292,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } + private fun MutableList.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf( + element = WalletNotification.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ), + condition = shouldShowPromo, + ) + } + // private fun MutableList.addYieldSupplyNotifications( // flattenCurrencies: Lce>, // ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 5d67cf05e5..9abf719a45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -396,6 +396,23 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + iconResId = R.drawable.ic_yield_promo_36, + onCloseClick = onCloseClick, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.notification_yield_promo_button), + onClick = onTermsAndConditionsClick, + ), + iconSize = 36.dp, + ), + ) + data class PushNotifications( val onCloseClick: () -> Unit, val onEnabledClick: () -> Unit, diff --git a/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml b/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml new file mode 100644 index 0000000000..4a4921466d --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml @@ -0,0 +1,15 @@ + + + + From 5a8ebb312c7fadded0381db6bb5e200d1df6d3a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 19:25:42 +0200 Subject: [PATCH 10/28] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index cfc2c0ab47..28079b094a 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit cfc2c0ab47e2e821f38f463f32f120d620804c72 +Subproject commit 28079b094a4518de068161226576747089519918 From f8fbb352da47e5a401cadcd7462fcc33124f3ecb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 18:27:15 +0100 Subject: [PATCH 11/28] Updated on 2026-08-14 --- .../UpdateGlobalFeedStateTransformer.kt | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt index d6e5ec6be2..dc78f2b7a0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt @@ -25,15 +25,19 @@ internal class UpdateGlobalFeedStateTransformer( prevState.news.newsUMState == NewsUMState.ERROR val hasLoadingInCharts = loadingStatesByOrder.values.any { it } - val hasErrorInCharts = errorStatesByOrder.values.any { it != null } - val hasAllErrorInCharts = errorStatesByOrder.values.isNotEmpty() && errorStatesByOrder.values.all { it != null } + val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null + val hasErrorInMarketPulseChart = errorStatesByOrder + .any { it.key != SortByTypeUM.Rating && it.value != null } val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } - val areAllChartsError = hasAllErrorInCharts || hasErrorInCharts && hasLoadingInCharts + val areAllChartsError = when { + hasErrorInMarketChart && hasErrorInMarketPulseChart -> true + hasErrorInMarketPulseChart && hasLoadingInCharts -> true + hasErrorInMarketChart && hasLoadingInCharts -> true + else -> false + } val newGlobalState = when { - isNewsLoading && areAllChartsError -> GlobalFeedState.Loading - isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading isNewsError && areAllChartsLoading -> GlobalFeedState.Loading isNewsError && areAllChartsError -> { if (previousGlobalState !is GlobalFeedState.Error) { @@ -43,18 +47,11 @@ internal class UpdateGlobalFeedStateTransformer( onRetryClicked = onRetryClicked, ) } + isNewsLoading && areAllChartsError -> GlobalFeedState.Loading + isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading else -> GlobalFeedState.Content } - - return prevState.copy( - globalState = newGlobalState, - news = prevState.news.copy( - newsUMState = when { - isNewsError -> NewsUMState.ERROR - else -> NewsUMState.CONTENT - }, - ), - ) + return prevState.copy(globalState = newGlobalState) } private fun sendErrorAnalytics() { From 047a48eaf0dcaf082bc7b1e189d8251eb60ef934 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 20:48:10 +0300 Subject: [PATCH 12/28] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 3 +++ .../DefaultUserWalletsListRepository.kt | 6 +++++ .../core/analytics/models/AnalyticsParam.kt | 1 + .../domain/wallets/analytics/Settings.kt | 12 ++++++++++ .../analytics/WalletSettingsAnalyticEvents.kt | 5 ++++ .../wallets/usecase/SaveWalletUseCase.kt | 23 +++++++++++++++---- .../features/details/utils/UserWalletSaver.kt | 7 +++++- .../CreateHardwareWalletModel.kt | 5 +++- .../model/MultiWalletFinalizeModel.kt | 2 ++ .../model/OnboardingVisaInProgressModel.kt | 6 ++++- .../analytics/WalletScreenAnalyticsEvent.kt | 2 +- .../welcome/impl/model/WelcomeModel.kt | 22 ++++++++++-------- 12 files changed, 76 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 374af96dd0..ba29372fdb 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -129,12 +130,14 @@ internal object WalletsDomainModule { userWalletsListRepository: UserWalletsListRepository, hotWalletFeatureToggles: HotWalletFeatureToggles, walletsRepository: WalletsRepository, + analyticsEventHandler: AnalyticsEventHandler, ): SaveWalletUseCase { return SaveWalletUseCase( userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, walletsRepository = walletsRepository, useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + analyticsEventHandler = analyticsEventHandler, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 80dce033a7..414ea8a0b3 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -21,6 +21,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.wallets.R +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletPasswordRequester @@ -390,6 +391,7 @@ internal class DefaultUserWalletsListRepository( if (newUserWallet.walletId == oldUserWallet.walletId && oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold ) { + trackWalletUpgradeEvent() removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId)) // When upgrading from Hot to Cold, if biometric lock is available, set it if (hasBiometry()) { @@ -536,4 +538,8 @@ internal class DefaultUserWalletsListRepository( ), ) } + + private fun trackWalletUpgradeEvent() { + analyticsEventHandler.send(event = WalletSettingsAnalyticEvents.WalletUpgraded()) + } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 9488b9c840..cff2fe1375 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -85,6 +85,7 @@ sealed class AnalyticsParam { data object ImportWallet : ScreensSources("Import Wallet") data object CreateWalletIntro : ScreensSources("Create Wallet Intro") data object AddNewWallet : ScreensSources("Add New Wallet") + data object AddNew : ScreensSources("Add New") data object CreateWallet : ScreensSources("Create Wallet") } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt index 703dcb8cbb..7f8f5b9916 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt @@ -1,6 +1,9 @@ package com.tangem.domain.wallets.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.OneTimeAnalyticsEvent sealed class Settings( category: String = "Settings", @@ -9,4 +12,13 @@ sealed class Settings( ) : AnalyticsEvent(category, event, params) { class ButtonManageTokens : Settings(event = "Button - Manage Tokens") + + class ColdWalletAdded( + source: AnalyticsParam.ScreensSources?, + ) : Settings( + event = "Cold Wallet Added", + params = mapOf(AnalyticsParam.SOURCE to (source?.value ?: "Unknown")), + ), OneTimeAnalyticsEvent, AppsFlyerIncludedEvent { + override val oneTimeEventId: String = id + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 1c23c85475..be4e913124 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS +import com.tangem.core.analytics.models.AppsFlyerIncludedEvent sealed class WalletSettingsAnalyticEvents( category: String = "Settings / Wallet Settings", @@ -166,6 +167,10 @@ sealed class WalletSettingsAnalyticEvents( event = "Button - Start Upgrade", ) + class WalletUpgraded : WalletSettingsAnalyticEvents( + event = "Wallet Upgraded", + ), AppsFlyerIncludedEvent + enum class RecoveryPhraseScreenAction(val value: String) { Upgrade("Upgrade"), Backup("Backup"), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 354d0b6bc8..75f322f635 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -6,9 +6,12 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.analytics.Settings import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository @@ -16,8 +19,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet * - * @property userWalletsListManager user wallets list manager - * [REDACTED_AUTHOR] */ class SaveWalletUseCase( @@ -25,14 +26,21 @@ class SaveWalletUseCase( private val userWalletsListRepository: UserWalletsListRepository, private val walletsRepository: WalletsRepository, private val useNewRepository: Boolean, + private val analyticsEventHandler: AnalyticsEventHandler, ) { - suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { + suspend operator fun invoke( + userWallet: UserWallet, + canOverride: Boolean = false, + analyticsSource: AnalyticsParam.ScreensSources? = null, + ): Either { return if (useNewRepository) { either { val newUserWallet = userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } - val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride) + .onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) } + .bind() if (newUserWallet) { when (userWallet) { @@ -76,4 +84,11 @@ class SaveWalletUseCase( } } } + + private suspend fun trackColdWalletAddedIfNeeded(source: AnalyticsParam.ScreensSources?, userWallet: UserWallet) { + val hasHotWallet = userWalletsListRepository.userWalletsSync().any { it is UserWallet.Hot } + if (hasHotWallet && userWallet is UserWallet.Cold) { + analyticsEventHandler.send(event = Settings.ColdWalletAdded(source)) + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 04d7e91736..59be5a0529 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -75,7 +75,12 @@ internal class UserWalletSaver @Inject constructor( private suspend fun Raise.saveWallet(userWallet: UserWallet) { fold( - block = { saveWalletUseCase(userWallet).bind() }, + block = { + saveWalletUseCase( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.Settings, + ).bind() + }, recover = { error -> when (error) { is SaveWalletError.WalletAlreadySaved -> { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 39a2c8b242..5d32c8952b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -133,7 +133,10 @@ internal class CreateHardwareWalletModel @Inject constructor( return } - saveWalletUseCase(userWallet = userWallet).fold( + saveWalletUseCase( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.AddNew, + ).fold( ifLeft = { saveWalletError -> delay(HIDE_PROGRESS_DELAY) setLoading(false) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 4c9424cf98..fab52080ae 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -267,6 +268,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, ) userWalletCreated } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 235c201961..abc373283f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -173,7 +174,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } val userWallet = createUserWallet(params.scanResponse, newTokens) - saveWalletUseCase(userWallet) + saveWalletUseCase( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, + ) visaAuthTokenStorage.remove(params.scanResponse.card.cardId) otpStorage.removeOTP(params.scanResponse.card.cardId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 1eb6944175..e766bf4333 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -72,7 +72,7 @@ sealed class WalletScreenAnalyticsEvent { } put("Wallet Type", seedPhrase) }, - ) + ), AppsFlyerIncludedEvent data class NoticeFinishActivation( private val activationState: ActivationState, diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 4179ffefe8..83ab025490 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -197,18 +197,20 @@ internal class WelcomeModel @Inject constructor( onSuccess = { scanResponse -> val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() ?: return@scan - saveWalletUseCase.invoke(userWallet) - .onLeft { error -> - if (error is SaveWalletError.WalletAlreadySaved) { - userWalletsListRepository.unlock( - userWallet.walletId, - unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), - ).onRight { - userWalletsListRepository.select(userWallet.walletId) - router.replaceAll(AppRoute.Wallet) - } + saveWalletUseCase.invoke( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.SignIn, + ).onLeft { error -> + if (error is SaveWalletError.WalletAlreadySaved) { + userWalletsListRepository.unlock( + userWallet.walletId, + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + ).onRight { + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) } } + } .onRight { router.replaceAll(AppRoute.Wallet) } From e7f607001b952e817d55f7ea9279d95f4585176c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 21:16:20 +0300 Subject: [PATCH 13/28] Updated on 2026-08-14 --- .../features/home/impl/model/HomeModel.kt | 5 ++++- .../model/Wallet1ChooseOptionModel.kt | 22 +++++++++++-------- .../model/MultiWalletCreateWalletModel.kt | 21 ++++++++++-------- .../model/MultiWalletUpgradeWalletModel.kt | 22 +++++++++++-------- .../model/OnboardingNoteCreateWalletModel.kt | 6 ++++- .../v2/twin/impl/model/OnboardingTwinModel.kt | 2 ++ 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index e650912dc6..b261bf588d 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -190,7 +190,10 @@ internal class HomeModel @Inject constructor( return } - saveWalletUseCase(userWallet).fold( + saveWalletUseCase( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.Intro, + ).fold( ifLeft = { delay(HIDE_PROGRESS_DELAY) setLoading(false) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index 8d508c9a4b..32c406b3b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.mo import androidx.compose.runtime.Stable import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -50,17 +51,20 @@ internal class Wallet1ChooseOptionModel @Inject constructor( val scanResponse = params.multiWalletState.value.currentScanResponse val userWallet = createUserWallet(scanResponse) - saveWalletUseCase(userWallet, canOverride = true) - .onRight { - cardRepository.finishCardActivation(scanResponse.card.cardId) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, + ).onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) - // save user wallet for manage tokens screen - params.multiWalletState.update { - it.copy(resultUserWallet = userWallet) - } - - returnToParentFlow.emit(Unit) + // save user wallet for manage tokens screen + params.multiWalletState.update { + it.copy(resultUserWallet = userWallet) } + + returnToParentFlow.emit(Unit) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 1cb37f4eb6..42d2db0372 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -135,17 +135,20 @@ internal class MultiWalletCreateWalletModel @Inject constructor( val scanResponse = params.multiWalletState.value.currentScanResponse val userWallet = createUserWallet(scanResponse) - saveWalletUseCase(userWallet, canOverride = true) - .onRight { - cardRepository.finishCardActivation(scanResponse.card.cardId) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, + ).onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) - // save user wallet for manage tokens screen - params.multiWalletState.update { - it.copy(resultUserWallet = userWallet) - } - - onDone.emit(Step.Done) + // save user wallet for manage tokens screen + params.multiWalletState.update { + it.copy(resultUserWallet = userWallet) } + + onDone.emit(Step.Done) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt index f6b6306cd6..d7e2c6e3e9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -134,17 +135,20 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor( val scanResponse = params.multiWalletState.value.currentScanResponse val userWallet = createUserWallet(scanResponse) - saveWalletUseCase(userWallet, canOverride = true) - .onRight { - cardRepository.finishCardActivation(scanResponse.card.cardId) + saveWalletUseCase( + userWallet, + canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, + ).onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) - // save user wallet for manage tokens screen - params.multiWalletState.update { - it.copy(resultUserWallet = userWallet) - } - - onDone.emit(Step.Done) + // save user wallet for manage tokens screen + params.multiWalletState.update { + it.copy(resultUserWallet = userWallet) } + + onDone.emit(Step.Done) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 51db083121..b286fead4f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -82,7 +82,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( private fun createWalletAndNavigateBackWithDone(scanResponse: ScanResponse) { modelScope.launch { val userWallet = createUserWallet(scanResponse) - saveWalletUseCase(userWallet, canOverride = true).onRight { + saveWalletUseCase( + userWallet, + canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, + ).onRight { params.onWalletCreated(userWallet) } _uiState.update { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index e70843bd7b..79c8741734 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -300,6 +300,7 @@ internal class OnboardingTwinModel @Inject constructor( saveWalletUseCase( userWallet = userWallet, canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, ).onLeft { Timber.e("Unable to save user wallet: $it") setLoading(false) @@ -329,6 +330,7 @@ internal class OnboardingTwinModel @Inject constructor( saveWalletUseCase( userWallet = userWallet, canOverride = true, + analyticsSource = AnalyticsParam.ScreensSources.Onboarding, ).onLeft { Timber.e("Unable to save user wallet: $it") setLoading(false) From df39e1703c51ac400059f88a8d44d8bd732a02b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 23:28:15 +0400 Subject: [PATCH 14/28] Updated on 2026-08-14 --- .../wallet/deeplink/DefaultPromoDeeplinkHandler.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index af455b27ac..0cb63b07c9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -59,14 +59,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( init { analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart()) + + saveAndBindRefcode() + val promoCode = queryParams[PROMO_CODE_KEY].orEmpty() if (promoCode.isEmpty()) { showAlert(InvalidPromoCode) } else { findSelectedWallet(promoCode) } - - saveAndBindRefcode() } private fun findSelectedWallet(promoCode: String) { @@ -183,14 +184,19 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( message = message, dismissOnFirstAction = true, firstActionBuilder = { - okAction { } + okAction { + uiMessageSender.send(GlobalLoadingMessage(false)) + } }, ), ) } + @Suppress("NullableToStringCall") private fun saveAndBindRefcode() { scope.launch(dispatchers.default) { + Timber.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign") + if (!refcode.isNullOrBlank()) { val conversionData = AppsFlyerConversionData(refcode = refcode, campaign = campaign) From b4f9d080e529b6b7dc54ac11819a7a4c1c49e456 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 11:30:28 +0400 Subject: [PATCH 15/28] Updated on 2026-08-14 --- .../tangem/feature/swap/model/SwapModel.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 1fab8b4bc9..a007a6697f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -219,9 +219,17 @@ internal class SwapModel @Inject constructor( private val swapRouter: SwapRouter = SwapRouter(router = router) private var userCountry: UserCountry? = null - private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus + private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null + /** + * If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null + * If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null + * + * Remove when accounts are integrated into Tangem Pay + */ + private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null + private val isUserResolvableError: (SwapState) -> Boolean = { swapState -> swapState is SwapState.SwapError && ( @@ -287,7 +295,7 @@ internal class SwapModel @Inject constructor( } modelScope.launch(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) { + if (canUseFromAccountCurrencyStatus) { isAccountsMode = isAccountsModeEnabledUseCase.invokeSync() val fromAccountStatus = getAccountCurrencyStatusUseCase.invokeSync( @@ -578,11 +586,11 @@ internal class SwapModel @Inject constructor( } else { initialFromStatus to selectedCurrency } - val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) { + val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) { if (isOrderReversed) { - selectedAccount to fromAccountCurrencyStatus.account + selectedAccount to requireNotNull(fromAccountCurrencyStatus).account } else { - fromAccountCurrencyStatus.account to selectedAccount + requireNotNull(fromAccountCurrencyStatus).account to selectedAccount } } else { null to null @@ -1261,7 +1269,7 @@ internal class SwapModel @Inject constructor( fromAccount = foundAccount toToken = initialFromStatus toAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus.account + fromAccountCurrencyStatus?.account } else { null } @@ -1279,7 +1287,7 @@ internal class SwapModel @Inject constructor( } else { fromToken = initialFromStatus fromAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus.account + fromAccountCurrencyStatus?.account } else { null } From e9e6b8bcc616811665646906e14bf8dbeff92f7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 12:22:49 +0300 Subject: [PATCH 16/28] Updated on 2026-08-14 --- .../core/ui/components/token/TokenItem.kt | 4 +- .../ui/components/tokenlist/TokenListItem.kt | 83 +++++++++++-------- .../organizetokens/OrganizeTokensScreen.kt | 5 +- .../MultiCurrencyAccountContent.kt | 7 +- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 631d330388..d76f2160fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -120,7 +120,7 @@ fun TokenItem( composables.icon.invoke( Modifier .layoutId(layoutId = LayoutId.ICON) - .padding(end = TangemTheme.dimens.spacing8) + .padding(end = TangemTheme.dimens.spacing12) .testTag(TokenElementsTestTags.TOKEN_ICON), ) } else { @@ -128,7 +128,7 @@ fun TokenItem( state = state.iconState, modifier = Modifier .layoutId(layoutId = LayoutId.ICON) - .padding(end = TangemTheme.dimens.spacing8) + .padding(end = TangemTheme.dimens.spacing12) .testTag(TokenElementsTestTags.TOKEN_ICON), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index ddfbb831bf..4b016c5544 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -7,8 +7,10 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon +import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +34,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ProvideSharedTransitionScope @@ -71,13 +74,13 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea ProvideSharedTransitionScope(modifier) { val iconSharedContentState = rememberSharedContentState(key = "icon_${state.id}") val titleSharedContentState = rememberSharedContentState(key = "title_${state.id}") - val boundsTransform = BoundsTransform { _, _ -> tween(350) } + val boundsTransform = BoundsTransform { _, _ -> tween(250) } AnimatedContent( targetState = state.isExpanded, transitionSpec = { - fadeIn(animationSpec = tween(350, delayMillis = 90)) - .togetherWith(fadeOut(animationSpec = tween(350))) + fadeIn(animationSpec = tween(250, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(250))) }, ) { isExpanded -> val animatedContentScope = this @@ -116,7 +119,7 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea val textSize by animateFloatAsState( targetValue = textStyle.fontSize.value, - animationSpec = tween(350), + animationSpec = tween(250), ) TokenTitle( @@ -133,32 +136,34 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea }, ) - if (isExpanded) { - ExpandedPortfolioHeader( - state = state.tokenItemUM, - isCollapsable = state.isCollapsable, - composables = composables, - balanceTextModifier = Modifier - .animateEnterExit( - enter = fadeIn(tween(350)) + - slideInHorizontally( - tween(350), - initialOffsetX = { fullWidth -> fullWidth * 2 }, - ), - exit = fadeOut( - tween(350), - ) + slideOutHorizontally( - tween(350), - targetOffsetX = { fullWidth -> fullWidth * 2 }, + CompositionLocalProvider(LocalRippleConfiguration provides null) { + if (isExpanded) { + ExpandedPortfolioHeader( + state = state.tokenItemUM, + isCollapsable = state.isCollapsable, + isBalanceHidden = isBalanceHidden, + composables = composables, + balanceTextModifier = Modifier + .animateEnterExit( + enter = fadeIn(tween(250)) + + slideInHorizontally( + tween(250), + initialOffsetX = { fullWidth -> fullWidth }, + ), + exit = fadeOut(tween(250)) + + slideOutHorizontally( + tween(250), + targetOffsetX = { fullWidth -> fullWidth }, + ), ), - ), - ) - } else { - TokenItem( - state = state.tokenItemUM, - isBalanceHidden = isBalanceHidden, - composables = composables, - ) + ) + } else { + TokenItem( + state = state.tokenItemUM, + isBalanceHidden = isBalanceHidden, + composables = composables, + ) + } } } } @@ -180,6 +185,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B @Composable fun ExpandedPortfolioHeader( state: TokenItemState, + isBalanceHidden: Boolean, isCollapsable: Boolean, composables: TokenItemComposables?, modifier: Modifier = Modifier, @@ -191,8 +197,10 @@ fun ExpandedPortfolioHeader( .fillMaxWidth() .clickable(onClick = { state.onItemClick?.invoke(state) }) .padding( - vertical = if (composables != null) 8.dp else 4.dp, - horizontal = 12.dp, + top = 12.dp, + bottom = 4.dp, + start = 12.dp, + end = 12.dp, ), ) { if (composables != null) { @@ -227,12 +235,19 @@ fun ExpandedPortfolioHeader( modifier = Modifier.weight(1f), ) { if (composables != null) { - composables.title.invoke(Modifier.alignByBaseline()) + composables.title.invoke( + Modifier + .padding(vertical = 2.dp) + .alignByBaseline(), + ) } else { when (val titleState = state.titleState) { TokenItemState.TitleState.Loading -> Unit TokenItemState.TitleState.Locked -> Unit is TokenItemState.TitleState.Content -> Text( + modifier = Modifier + .padding(vertical = 2.dp) + .alignByBaseline(), text = titleState.text.resolveReference(), color = TangemTheme.colors.text.primary1, overflow = TextOverflow.Ellipsis, @@ -249,10 +264,10 @@ fun ExpandedPortfolioHeader( } Text( - text = text.orEmpty(), + text = text.orEmpty().orMaskWithStars(isBalanceHidden), modifier = balanceTextModifier .alignByBaseline() - .padding(horizontal = 8.dp), + .padding(horizontal = 4.dp), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption1, overflow = TextOverflow.Ellipsis, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 08a1123ea0..2996d749be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -219,9 +219,8 @@ private fun LazyItemScope.DraggableItem( state = item.tokenItemState, isCollapsable = false, composables = null, - modifier = modifierWithBackground - .padding(top = 8.dp) - .fillMaxWidth(), + isBalanceHidden = isBalanceHidden, + modifier = modifierWithBackground.fillMaxWidth(), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 66c94df727..1ddf7dd2cf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import androidx.compose.animation.* +import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope @@ -141,7 +142,7 @@ private fun LazyListScope.portfolioItem( lastIndexProxy = lastIndex return@LaunchedEffect } - delay(timeMillis = minOf(50 * tokens.size, 350).toLong()) + delay(timeMillis = minOf(50 * tokens.size, 250).toLong()) lastIndexProxy = 0 } @@ -179,13 +180,13 @@ private fun SlideInItemVisibility( enter = fadeIn( tween(100, delayMillis = delayEnter), ) + expandVertically( - tween(100, delayMillis = delayEnter), + tween(100, delayMillis = delayEnter, easing = FastOutLinearInEasing), expandFrom = Alignment.Top, ), exit = fadeOut( tween(100, delayMillis = delayExit), ) + shrinkVertically( - tween(100, delayMillis = delayExit), + tween(100, delayMillis = delayExit, easing = FastOutLinearInEasing), shrinkTowards = Alignment.Top, ), ) { From a52c3623bd51f8e57310507310b170624d9f532b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 16:25:50 +0700 Subject: [PATCH 17/28] Updated on 2026-08-14 --- .../market/details/portfolio/add/AvailableToAddData.kt | 9 ++++++--- .../add/impl/converter/AvailableToAddDataConverter.kt | 1 - .../portfolio/add/impl/model/AddToPortfolioModel.kt | 5 +++-- .../markets/portfolio/add/api/AvailableToAddData.kt | 10 +++++++--- .../add/impl/converter/AvailableToAddDataConverter.kt | 1 - .../portfolio/add/impl/model/AddToPortfolioModel.kt | 5 +++-- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt index 9d221287dd..b868b9b8be 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt @@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable data class AvailableToAddData( val availableToAddWallets: Map, ) { - val isAvailableToAdd: Boolean - get() = availableToAddWallets.isNotEmpty() + val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd } val isSinglePortfolio: Boolean get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 } @@ -23,7 +22,9 @@ data class AvailableToAddWallet( val accounts: List, val availableNetworks: Set, val availableToAddAccounts: Map, -) +) { + val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd } +} @Serializable data class AvailableToAddAccount( @@ -38,6 +39,8 @@ data class AvailableToAddAccount( .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } .toSet() + val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() + val addedMarketNetworks: Set = availableNetworks .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } .toSet() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index 49d3820aa2..a6b02044fb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor( val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null accountStatus.account.accountId to availableToAddAccount } - .filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() } .toMap() return AvailableToAddWallet( userWallet = wallet, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index 72b3e4b07b..299f1d72ed 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -344,9 +344,10 @@ internal class AddToPortfolioModel @Inject constructor( portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] ?: return@isEnabled false - val availableAccount = + val isAvailableAccount = availableWallet.availableToAddAccounts[accountStatus.account.accountId] - return@isEnabled availableAccount != null + ?.isAvailableToAdd == true + return@isEnabled isAvailableAccount } AddToPortfolioManager.State.Init, AddToPortfolioManager.State.NothingToAdd, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt index 3ca633edf5..976ad0a6c4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt @@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable internal data class AvailableToAddData( val availableToAddWallets: Map, ) { - val isAvailableToAdd: Boolean - get() = availableToAddWallets.isNotEmpty() + val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd } val isSinglePortfolio: Boolean get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 } @@ -23,7 +22,9 @@ internal data class AvailableToAddWallet( val accounts: List, val availableNetworks: Set, val availableToAddAccounts: Map, -) +) { + val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd } +} @Serializable internal data class AvailableToAddAccount( @@ -31,6 +32,7 @@ internal data class AvailableToAddAccount( val availableNetworks: Set, val addedNetworks: Set, ) { + val isSingleNetwork: Boolean get() = availableNetworks.size == 1 @@ -38,6 +40,8 @@ internal data class AvailableToAddAccount( .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } .toSet() + val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() + val addedMarketNetworks: Set = availableNetworks .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } .toSet() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index a9a11d6eb7..3a311b6854 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor( val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null accountStatus.account.accountId to availableToAddAccount } - .filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() } .toMap() return AvailableToAddWallet( userWallet = wallet, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index b0198b5975..6b0fd6a169 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -334,9 +334,10 @@ internal class AddToPortfolioModel @Inject constructor( portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] ?: return@isEnabled false - val availableAccount = + val isAvailableAccount = availableWallet.availableToAddAccounts[accountStatus.account.accountId] - return@isEnabled availableAccount != null + ?.isAvailableToAdd == true + return@isEnabled isAvailableAccount } AddToPortfolioManager.State.Init, AddToPortfolioManager.State.NothingToAdd, From e358198711825a118e2f13f928afbd45eef7c3ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 09:37:57 +0000 Subject: [PATCH 18/28] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d7281ac75f..9f554a7f91 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1404" +tangemBlockchainSdk = "releases-5.33-1406" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.32-574" +tangemCardSdk = "releases-5.33-576" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-541" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 550883cc8847f825937c252ed0c00a57a2021b0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 13:46:00 +0400 Subject: [PATCH 19/28] Updated on 2026-08-14 --- .../java/com/tangem/tap/routing/utils/ChildFactory.kt | 4 +--- .../src/main/kotlin/com/tangem/common/routing/AppRoute.kt | 4 +--- .../tangempay/components/TangemPayOnboardingComponent.kt | 4 +--- .../features/tangempay/model/TangemPayOnboardingModel.kt | 8 +------- .../child/wallet/model/intents/TangemPayClickIntents.kt | 2 +- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index f182a2555b..cb500685e1 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -691,9 +691,7 @@ internal class ChildFactory @Inject constructor( deeplink = mode.deeplink, ) is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings - is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain( - userWalletId = mode.userWalletId, - ) + is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain }, componentFactory = tangemPayOnboardingComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 2be124d092..4b9bd5496d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -444,9 +444,7 @@ sealed class AppRoute(val path: String) : Route { ) : Mode() @Serializable - data class FromBannerOnMain( - val userWalletId: UserWalletId, - ) : Mode() + data object FromBannerOnMain : Mode() @Serializable data object FromBannerInSettings : Mode() diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index df83da98da..e8c5c6d08d 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -16,9 +16,7 @@ interface TangemPayOnboardingComponent : ComposableContentComponent { val userWalletId: UserWalletId, ) : Params() - data class FromBannerOnMain( - val userWalletId: UserWalletId, - ) : Params() + data object FromBannerOnMain : Params() data object FromBannerInSettings : Params() } diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index a3dc4ea532..b8ce2a6fac 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -131,13 +131,7 @@ internal class TangemPayOnboardingModel @Inject constructor( is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { checkCustomerInfo(params.userWalletId) } - is TangemPayOnboardingComponent.Params.FromBannerOnMain -> { - if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) { - checkCustomerInfo(userWalletId = params.userWalletId) - } else { - openWalletSelectorIfNeeds(eligibleWalletsIds) - } - } + is TangemPayOnboardingComponent.Params.FromBannerOnMain, is TangemPayOnboardingComponent.Params.Deeplink, is TangemPayOnboardingComponent.Params.FromBannerInSettings, -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 57040c28d4..1318303b1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -243,7 +243,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( modelScope.launch { val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { - router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) + router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain) } else { stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId)) } From 95ebe65e573899b3f411a3b0f30efec546ba8539 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 13:08:39 +0300 Subject: [PATCH 20/28] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++++------ .../tangem/feature/swap/DefaultSwapComponent.kt | 14 ++++---------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 286471b0a0..217144c02a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1535,9 +1535,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - spenderAddress = requireNotNull(quoteModel.allowanceContract) { - "allowanceContract is required for DEX" - }, + spenderAddress = quoteModel.allowanceContract, ) if (state !is SwapState.QuotesLoadedState) return state state.copy( @@ -2079,14 +2077,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) ?: TxFeeState.Empty } - @Suppress("LongParameterList", "LongMethod") + @Suppress("LongParameterList", "LongMethod", "CanBeNonNullable") private suspend fun updatePermissionState( networkId: String, fromTokenStatus: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, - spenderAddress: String, + spenderAddress: String?, isAllowedToSpend: Boolean, ): SwapState { val fromToken = fromTokenStatus.currency @@ -2110,7 +2108,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val derivationPath = fromToken.network.derivationPath.value // setting up amount for approve with given amount for swap [SwapApproveType.Limited] val callData = SmartContractCallDataProviderFactory.getApprovalCallData( - spenderAddress = spenderAddress, + spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" }, amount = swapAmount.value.convertToSdkAmount(fromTokenStatus), blockchain = fromToken.network.toBlockchain(), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 55e17a1292..6e9495b238 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -2,9 +2,7 @@ package com.tangem.feature.swap import androidx.compose.animation.Crossfade import androidx.compose.foundation.background -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext @@ -103,13 +101,9 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { if (sendFeatureToggles.isGaslessTransactionsEnabled) { - val fromCryptoCurrency = if (model.isOrderReversed) { - model.dataState.toCryptoCurrency - } else { - model.dataState.fromCryptoCurrency - } - val feePaidCryptoCurrency = model.dataState.feePaidCryptoCurrency - val amount = model.dataState.amount + val fromCryptoCurrency by remember { derivedStateOf { model.dataState.fromCryptoCurrency } } + val feePaidCryptoCurrency by remember { derivedStateOf { model.dataState.feePaidCryptoCurrency } } + val amount by remember { derivedStateOf { model.dataState.amount } } LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, amount.isNullOrBlank()) { if (amount.isNullOrBlank()) { From 050f25035b0b6d34bb36fc13d1c4102345a54e4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 18:24:03 +0500 Subject: [PATCH 21/28] Updated on 2026-08-14 --- .../features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index fe05161fec..9bc9fb3314 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -70,7 +70,6 @@ internal object SwapAmountQuoteUtils { } return copy( - isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled, primaryAmount = updatedAmountField, ) } From d9744e19ec5c3954a4041796082a912db9774505 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 16:37:56 +0300 Subject: [PATCH 22/28] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index abf365f0c3..d1e7a735d9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1408" +tangemBlockchainSdk = "releases-5.33-1407" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-577" +tangemCardSdk = "releases-5.33-576" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From a540a98d015731c8e281368749134cd38615f95b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 18:24:49 +0300 Subject: [PATCH 23/28] Updated on 2026-08-14 --- .../sendviaswap/confirm/SendWithSwapConfirmComponent.kt | 2 +- .../sendviaswap/confirm/model/SendWithSwapConfirmModel.kt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 5d53b1d40e..579a4ddafc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -179,7 +179,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( val swapDirection: SwapDirection, val isBalanceHidingFlow: StateFlow, val primaryCryptoCurrencyStatusFlow: StateFlow, - val primaryFeePaidCurrencyStatusFlow: StateFlow, + val primaryFeePaidCurrencyStatusFlow: StateFlow, // doesn't change if select gasless fee val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val callback: ModelCallback, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 66d822dd46..97a3d13b93 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -22,10 +22,10 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.express.models.ExpressOperationType -import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase @@ -395,6 +395,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( private fun updateConfirmNotifications() { modelScope.launch { + val feeCryptoCurrencyStatus = + feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: params.primaryFeePaidCurrencyStatusFlow.value sendNotificationsUpdateTrigger.triggerUpdate( data = NotificationData( destinationAddress = when (val currency = primaryCurrencyStatus.currency) { @@ -407,7 +409,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( isIgnoreReduce = confirmData.isIgnoreReduce, fee = confirmData.fee, feeError = confirmData.feeError, - feeCryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ), ) swapNotificationsUpdateTrigger.triggerUpdate( From 47d119c2dabe33eaf4fcad2ec555d6a656eddf28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 18:48:42 +0400 Subject: [PATCH 24/28] Updated on 2026-08-14 --- .../fetcher/DefaultWalletAccountsFetcher.kt | 6 +++ .../DefaultWalletAccountsFetcherTest.kt | 42 +++++++++++++++++++ .../common/account/WalletAccountsFetcher.kt | 4 ++ data/onramp/build.gradle.kts | 1 + .../data/onramp/DefaultHotCryptoRepository.kt | 11 ++++- .../tangem/data/onramp/di/OnrampDataModule.kt | 6 +++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 68bce0a025..b75e956b6e 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -24,6 +24,8 @@ import com.tangem.datasource.api.tangemTech.models.orDefault import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -77,6 +79,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( return migratedResponse } + override fun get(userWalletId: UserWalletId): Flow { + return getAccountsResponseStore(userWalletId = userWalletId).data.filterNotNull() + } + override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? { return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull() } diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt index 49586e40b0..1dfc937810 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -20,6 +20,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow @@ -340,6 +341,47 @@ class DefaultWalletAccountsFetcherTest { } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Get { + + @Test + fun get() = runTest { + // Arrange + val response = createGetWalletAccountsResponse(userWalletId = userWalletId) + accountsResponseStoreFlow.value = response + + // Act + val actual = getEmittedValues(fetcher.get(userWalletId)) + + // Assert + val expected = response + Truth.assertThat(actual).containsExactly(expected) + + coVerifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + } + } + + @Test + fun `get if store is empty`() = runTest { + // Arrange + accountsResponseStoreFlow.value = null + + // Act + val actual = getEmittedValues(fetcher.get(userWalletId)) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerifyOrder { + accountsResponseStoreFactory.create(userWalletId) + accountsResponseStore.data + } + } + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Store { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt index 4004ed3fcf..81d7900564 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.data.common.account import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Component for fetching wallet accounts @@ -14,6 +15,9 @@ interface WalletAccountsFetcher { @Throws suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse + /** Get wallet accounts by [userWalletId] */ + fun get(userWalletId: UserWalletId): Flow + /** Get saved wallet accounts by [userWalletId] */ suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? } \ No newline at end of file diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index ada7a8f3b0..bf0adddbac 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.data.common) /** Domain modules */ + implementation(projects.domain.account) implementation(projects.domain.onramp) implementation(projects.domain.legacy) implementation(projects.domain.card) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index b1552db7cc..5b985a43fc 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -7,16 +7,19 @@ import com.tangem.blockchainsdk.utils.OLD_POLYGON_NAME import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.models.wallet.UserWallet @@ -54,7 +57,9 @@ internal class DefaultHotCryptoRepository( private val userWalletsStore: UserWalletsStore, private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, + private val accountsFeatureToggles: AccountsFeatureToggles, private val userTokensResponseStore: UserTokensResponseStore, + private val walletAccountsFetcher: WalletAccountsFetcher, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, ) : HotCryptoRepository { @@ -100,7 +105,11 @@ internal class DefaultHotCryptoRepository( private fun getWalletsWithTokensFlow(): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> - userTokensResponseStore.get(userWalletId = userWallet.walletId) + if (accountsFeatureToggles.isFeatureEnabled) { + walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } + } else { + userTokensResponseStore.get(userWalletId = userWallet.walletId) + } .map { userWallet to it?.tokens.orEmpty() } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 2e80138833..a4e93fec4f 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.onramp.di import com.squareup.moshi.Moshi import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.onramp.DefaultHotCryptoRepository import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository @@ -28,6 +29,7 @@ import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -109,6 +111,8 @@ internal object OnrampDataModule { dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, userTokensResponseStore: UserTokensResponseStore, + accountsFeatureToggles: AccountsFeatureToggles, + walletAccountsFetcher: WalletAccountsFetcher, ): HotCryptoRepository { return DefaultHotCryptoRepository( excludedBlockchains = excludedBlockchains, @@ -119,6 +123,8 @@ internal object OnrampDataModule { dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, userTokensResponseStore = userTokensResponseStore, + accountsFeatureToggles = accountsFeatureToggles, + walletAccountsFetcher = walletAccountsFetcher, ) } From 99368d9560a4922cccef2f8e80530a4195854ee7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 12:10:04 +0300 Subject: [PATCH 25/28] Updated on 2026-08-14 --- .../feature/swap/DefaultSwapComponent.kt | 7 ++-- .../tangem/feature/swap/model/SwapModel.kt | 38 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 6e9495b238..1ab3e27465 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -101,9 +101,10 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { if (sendFeatureToggles.isGaslessTransactionsEnabled) { - val fromCryptoCurrency by remember { derivedStateOf { model.dataState.fromCryptoCurrency } } - val feePaidCryptoCurrency by remember { derivedStateOf { model.dataState.feePaidCryptoCurrency } } - val amount by remember { derivedStateOf { model.dataState.amount } } + val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle() + val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } } + val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } } + val amount by remember { derivedStateOf { dataState.amount } } LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, amount.isNullOrBlank()) { if (amount.isNullOrBlank()) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 8f5bd07b10..fd342433db 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -80,7 +80,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.AddToPortfolioRoute -import com.tangem.feature.swap.models.SwapCardState import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager @@ -200,7 +199,12 @@ internal class SwapModel @Inject constructor( private val searchDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() - var dataState by mutableStateOf(SwapProcessDataState()) + val dataStateStateFlow = MutableStateFlow(SwapProcessDataState()) + var dataState + get() = dataStateStateFlow.value + set(value) { + dataStateStateFlow.value = value + } var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( @@ -699,6 +703,7 @@ internal class SwapModel @Inject constructor( toProvidersList: List, updateFeeBlock: Boolean = true, ): PeriodicTask> { + var shouldUpdateFeeBlock = updateFeeBlock return PeriodicTask( delay = UPDATE_DELAY, task = { @@ -734,8 +739,10 @@ internal class SwapModel @Inject constructor( tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) - if (updateFeeBlock) { + if (shouldUpdateFeeBlock) { modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() } + } else { + shouldUpdateFeeBlock = true } } else { feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) @@ -2140,20 +2147,19 @@ internal class SwapModel @Inject constructor( ) } + @Suppress("UnsafeCallOnNullableType") inner class FeeSelectorRepository : SwapFeeSelectorBlockComponent.ModelRepositoryExtended { - override val state = MutableStateFlow(FeeSelectorUM.Loading) + override val state = MutableStateFlow( + FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), + ) override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { - val sendCardData = - uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) - val receiveCardData = - uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) - val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError) - val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError) - val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError) + val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) + val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) + val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!! if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { return Either.Left(GetFeeError.UnknownError) @@ -2209,13 +2215,9 @@ internal class SwapModel @Inject constructor( } override suspend fun loadFee(): Either { - val sendCardData = - uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) - val receiveCardData = - uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) - val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError) - val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError) - val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError) + val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) + val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) + val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!! if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { return Either.Left(GetFeeError.UnknownError) From 699606ad3eaf2fa925b8ef80913d2f749ab911c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 14:32:48 +0500 Subject: [PATCH 26/28] Updated on 2026-08-14 --- .../preview/PreviewUserWalletListComponent.kt | 1 - .../details/entity/UserWalletListUM.kt | 2 - .../details/model/UserWalletListModel.kt | 1 - .../details/ui/UserWalletListBlock.kt | 47 ++++++++++++------- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index e56710ead0..86ecb2a42f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -50,7 +50,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { ), addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), isWalletSavingInProgress = true, - addNewWalletIconRes = R.drawable.ic_plus_24, onAddNewWalletClick = {}, ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index bc4baf7d42..a8ef5eb141 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,6 +1,5 @@ package com.tangem.features.details.entity -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference @@ -11,6 +10,5 @@ internal data class UserWalletListUM( val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, - @DrawableRes val addNewWalletIconRes: Int?, val onAddNewWalletClick: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index d1a90f171f..4a60a74a45 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -57,7 +57,6 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, - addNewWalletIconRes = R.drawable.ic_plus_24, ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index f06856c4df..e253ffec97 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,9 +1,10 @@ package com.tangem.features.details.ui import android.content.res.Configuration -import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -11,10 +12,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference @@ -24,6 +28,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.impl.R @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { @@ -42,7 +47,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M text = state.addNewWalletText, isInProgress = state.isWalletSavingInProgress, onClick = state.onAddNewWalletClick, - icon = state.addNewWalletIconRes, ) } } @@ -50,7 +54,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M @Composable private fun AddWalletButton( text: TextReference, - @DrawableRes icon: Int?, isInProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -67,22 +70,30 @@ private fun AddWalletButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - if (icon != null) { - AnimatedContent( - modifier = Modifier.size(TangemTheme.dimens.size24), - targetState = isInProgress, - label = "Add wallet progress", - ) { isInProgress -> - if (isInProgress) { - CircularProgressIndicator( - modifier = Modifier.size(TangemTheme.dimens.size24), - color = TangemTheme.colors.icon.accent, - ) - } else { + AnimatedContent( + modifier = Modifier.size(TangemTheme.dimens.size36), + targetState = isInProgress, + label = "Add wallet progress", + ) { isInProgress -> + if (isInProgress) { + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size24) + .padding(4.dp), + color = TangemTheme.colors.icon.accent, + ) + } else { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)), + ) { Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = icon), + modifier = Modifier.size(18.dp), tint = TangemTheme.colors.icon.accent, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_24), contentDescription = null, ) } From 42b4c894bbd6cd26bfca8ba04cdf6839ffa7e2a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 22:46:13 +0400 Subject: [PATCH 27/28] Updated on 2026-08-14 --- .../data/account/di/AccountDataModule.kt | 5 ++ .../DefaultMainAccountTokensMigration.kt | 47 +++++++++++-------- .../DefaultMainAccountTokensMigrationTest.kt | 5 ++ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index 10b55769a9..559ba4c6ce 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -16,6 +16,7 @@ import com.tangem.data.account.store.ArchivedAccountsStoreFactory import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.account.WalletAccountsSaver +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi @@ -117,11 +118,15 @@ internal object AccountDataModule { accountsResponseStoreFactory: AccountsResponseStoreFactory, userTokensSaver: UserTokensSaver, accountTokenMigrationStore: AccountTokenMigrationStore, + eTagsStore: ETagsStore, + dispatchers: CoroutineDispatcherProvider, ): DefaultMainAccountTokensMigration { return DefaultMainAccountTokensMigration( accountsResponseStoreFactory = accountsResponseStoreFactory, accountTokenMigrationStore = accountTokenMigrationStore, userTokensSaver = userTokensSaver, + eTagsStore = eTagsStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index f03409232e..3973b17662 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -11,6 +11,7 @@ import com.tangem.data.account.converter.AccountNameConverter import com.tangem.data.account.converter.toDerivationIndex import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.utils.assignTokens +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse @@ -22,6 +23,10 @@ import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import timber.log.Timber /** @@ -37,8 +42,12 @@ internal class DefaultMainAccountTokensMigration( private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val accountTokenMigrationStore: AccountTokenMigrationStore, private val userTokensSaver: UserTokensSaver, + private val eTagsStore: ETagsStore, + dispatchers: CoroutineDispatcherProvider, ) : MainAccountTokensMigration { + private val coroutineScope = CoroutineScope(dispatchers.default + SupervisorJob()) + internal suspend fun migrate(userWalletId: UserWalletId): Either = either { val store = accountsResponseStoreFactory.create(userWalletId) val response = store.getSyncOrNull() @@ -89,16 +98,7 @@ internal class DefaultMainAccountTokensMigration( store.updateData { updatedResponse } - val userTokensResponse = updatedResponse.toUserTokensResponse() - userTokensSaver.pushWithRetryer( - userWalletId = userWalletId, - response = userTokensResponse, - onFailSend = { - val exception = IllegalStateException("Failed to push updated tokens after migration") - Timber.e(exception) - raise(exception) - }, - ) + pushTokens(userWalletId, updatedResponse) return@either updatedResponse } @@ -151,16 +151,7 @@ internal class DefaultMainAccountTokensMigration( val selectedAccountName = AccountNameConverter.convertBack(selectedAccount.name) accountTokenMigrationStore.store(userWalletId, mainAccountName to selectedAccountName) - val userTokensResponse = updatedResponse.toUserTokensResponse() - userTokensSaver.pushWithRetryer( - userWalletId = userWalletId, - response = userTokensResponse, - onFailSend = { - val exception = IllegalStateException("Failed to push updated tokens after migration") - Timber.e(exception) - raise(exception) - }, - ) + pushTokens(userWalletId, updatedResponse) } private fun Raise.findAccount( @@ -232,4 +223,20 @@ internal class DefaultMainAccountTokensMigration( accountNodeValue == derivationIndex.value.toLong() } } + + private suspend fun Raise.pushTokens(userWalletId: UserWalletId, response: GetWalletAccountsResponse) { + val userTokensResponse = response.toUserTokensResponse() + userTokensSaver.pushWithRetryer( + userWalletId = userWalletId, + response = userTokensResponse, + onFailSend = { + coroutineScope.launch { + eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts) + } + val exception = IllegalStateException("Failed to push updated tokens after migration") + Timber.e(exception) + raise(exception) + }, + ) + } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt index 150d3ec917..70d14974b1 100644 --- a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt @@ -6,6 +6,7 @@ import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse @@ -17,6 +18,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.assertEither import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -34,11 +36,14 @@ class DefaultMainAccountTokensMigrationTest { private val accountsResponseStoreFlow = MutableStateFlow(value = null) private val accountTokenMigrationStore = mockk(relaxUnitFun = true) private val userTokensSaver = mockk(relaxUnitFun = true) + private val eTagsStore = mockk(relaxUnitFun = true) private val migration = DefaultMainAccountTokensMigration( accountsResponseStoreFactory = accountsResponseStoreFactory, accountTokenMigrationStore = accountTokenMigrationStore, userTokensSaver = userTokensSaver, + eTagsStore = eTagsStore, + dispatchers = TestingCoroutineDispatcherProvider(), ) private val userWalletId = UserWalletId("011") From ae7332f9555da970e8a4b8ea8a6fd2d0bc7d0d41 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 10:28:36 +0000 Subject: [PATCH 28/28] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d1e7a735d9..81757bf804 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1407" +tangemBlockchainSdk = "develop-1391" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-541" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^