From cb53b35084992e49fefb9d4173106770de39efd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 16:42:11 +0200 Subject: [PATCH 01/15] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++-- .../ui/components/notifications/OkxPromoNotification.kt | 2 +- fastlane/Fastfile | 6 +----- features/wallet/impl/build.gradle.kts | 2 +- .../wallet/ui/utils/ReviewManagerRequester.kt | 2 +- gradle/dependencies.toml | 8 ++++---- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8615f2502..18fd29dc28 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,8 +184,8 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) implementation(deps.material) - implementation(deps.googlePlay.core) - implementation(deps.googlePlay.core.ktx) + implementation(deps.googlePlay.review) + implementation(deps.googlePlay.review.ktx) implementation(deps.googlePlay.services.wallet) coreLibraryDesugaring(deps.desugar) implementation(deps.timber) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt index 9d6d403946..eacaa9567e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -58,7 +58,7 @@ private fun Content(config: NotificationConfig) { .align(Alignment.CenterVertically), ) Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), modifier = Modifier .weight(1f) .padding(TangemTheme.dimens.spacing12), diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 67b386e4e9..f18a200057 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,12 +23,8 @@ platform :android do desc "Runs all the tests" lane :test do - gradle( - task: "clean assemble", - build_type: "Debug" - ) gradle(task: "detekt") - gradle(task: "test") + gradle(task: "testDebugUnitTest") end desc "Build a signed release APK" diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index dad4976a7d..ecad0b9c68 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) - implementation(deps.googlePlay.core) + implementation(deps.googlePlay.review) implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt index a52e61dd42..1826afb74d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils import android.app.Activity import android.content.Context import android.content.ContextWrapper +import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManager import com.google.android.play.core.review.ReviewManagerFactory -import com.google.android.play.core.tasks.Task import timber.log.Timber internal object ReviewManagerRequester { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e73b861d62..41817d438f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -46,8 +46,8 @@ coroutine = "1.7.2" desugarJdkLibs = "1.1.5" firebase = "26.0.0" googleMaterialComponent = "1.6.1" -googlePlayCore = "1.10.3" -googlePlayCoreKtx = "1.8.1" +googlePlayReview = "2.0.1" +googlePlayReviewKtx = "2.0.1" googlePlayServicesWallet = "19.1.0" hilt = "2.46" hilt-navigation = "1.0.0" @@ -212,8 +212,8 @@ kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } -googlePlay-core = { module = "com.google.android.play:core", version.ref = "googlePlayCore" } -googlePlay-core-ktx = { module = "com.google.android.play:core-ktx", version.ref = "googlePlayCoreKtx" } +googlePlay-review = { module = "com.google.android.play:review", version.ref = "googlePlayReview" } +googlePlay-review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "googlePlayReviewKtx" } googlePlay-services-wallet = { module = "com.google.android.gms:play-services-wallet", version.ref = "googlePlayServicesWallet" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } From 231b70c00d89c3ded2bda65e92293c319bd60d1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 14 Jul 2024 01:36:52 +0200 Subject: [PATCH 02/15] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/tangem/tap/MainActivity.kt | 22 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 536e770195..2754eb4547 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ea644f792a..87b2bf414d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -2,13 +2,10 @@ package com.tangem.tap import android.Manifest import android.annotation.SuppressLint -import android.app.PendingIntent import android.content.Intent -import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration -import android.nfc.NfcAdapter import android.os.Build import android.os.Bundle import android.view.View @@ -298,31 +295,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - val pendingIntent = PendingIntent.getActivity( - this, - 0, - Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, - ) - val intentFilters = arrayOf( - IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), - ) - nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) navigateToInitialScreenIfNeeded(intent) } - override fun onPause() { - super.onPause() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - nfcAdapter.disableForegroundDispatch(this) - } - override fun onStop() { notificationsHandler = null dialogManager.onStop() From b716d2971925c19a394f5441f7dffc21bfbfa956 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 13:45:48 +0100 Subject: [PATCH 03/15] Updated on 2026-08-14 --- .../common/response/ApiResponseCallDelegate.kt | 15 +++++++++++++-- .../api/common/response/ApiResponseError.kt | 10 ++++++++-- .../datasource/api/common/response/ResponseExt.kt | 14 ++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 52a2e4cc6f..919be8e848 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -5,6 +5,7 @@ import okio.Timeout import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import timber.log.Timber internal class ApiResponseCallDelegate( private val wrappedCall: Call, @@ -20,7 +21,9 @@ internal class ApiResponseCallDelegate( override fun timeout(): Timeout = wrappedCall.timeout() override fun isExecuted(): Boolean = wrappedCall.isExecuted override fun isCanceled(): Boolean = wrappedCall.isCanceled - override fun cancel() { wrappedCall.cancel() } + override fun cancel() { + wrappedCall.cancel() + } private inner class ApiResponseCallback( private val responseCallback: Callback>, @@ -33,7 +36,15 @@ internal class ApiResponseCallDelegate( } override fun onFailure(call: Call, t: Throwable) { - val error = t.toApiError() + val error = try { + t.toApiError() + } catch (e: ApiResponseError) { + Timber.e(e, "error map toApiError") + e + } catch (e: Exception) { + Timber.e(e, "onFailure UnknownException") + ApiResponseError.UnknownException(e) + } val safeResponse = apiError(error) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index cfd2146441..22162b76b4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -73,10 +73,16 @@ sealed class ApiResponseError : Exception() { } /** Represents a network error, typically when there's no connectivity. */ - data object NetworkException : ApiResponseError() + @Suppress("UnusedPrivateMember") + data object NetworkException : ApiResponseError() { + private fun readResolve(): Any = NetworkException + } /** Represents a timeout error, typically when the server takes too long to respond. */ - data object TimeoutException : ApiResponseError() + @Suppress("UnusedPrivateMember") + data object TimeoutException : ApiResponseError() { + private fun readResolve(): Any = TimeoutException + } /** * Represents an unexpected exception that doesn't fall into one of the other categories. diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index 92a5af0ac5..f696275bc3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.response import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response +import timber.log.Timber import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -16,10 +17,15 @@ internal fun Response.toSafeApiResponse(): ApiResponse { } else { val code = ApiResponseError.HttpException.Code.values .firstOrNull { it.code == code() } - val e = if (code == null) { - ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) - } else { - ApiResponseError.HttpException(code, message(), errorBody()?.string()) + val e = try { + if (code == null) { + ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) + } else { + ApiResponseError.HttpException(code, message(), errorBody()?.string()) + } + } catch (e: Exception) { + Timber.e(e, "UnknownException occured") + ApiResponseError.UnknownException(e) } apiError(e) From 929177b90e74b9483757107c785686e5188ba89b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 12:07:21 +0100 Subject: [PATCH 04/15] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/common/DialogManager.kt | 3 ++- .../main/java/com/tangem/tap/common/redux/AppDialog.kt | 1 + .../impl/presentation/router/DefaultTokensListRouter.kt | 8 ++++++-- .../tokens/impl/presentation/router/TokensListRouter.kt | 3 ++- .../impl/presentation/viewmodels/TokensListViewModel.kt | 1 + .../notifications/NotificationWithBackground.kt | 4 ++-- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 6b870386be..01625dc4f2 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -139,8 +139,9 @@ class DialogManager : StoreSubscriber { title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), message = context.getString( state.dialog.messageRes, - state.dialog.currencySymbol, state.dialog.currencyTitle, + state.dialog.currencySymbol, + state.dialog.networkName, ), context = context, ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index fc362509c3..a65014a580 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -43,6 +43,7 @@ sealed class AppDialog : StateDialog { data class TokensAreLinkedDialog( val currencyTitle: String, val currencySymbol: String, + val networkName: String, ) : AppDialog() { val messageRes: Int = R.string.token_details_unable_hide_alert_message val titleRes: Int = R.string.token_details_unable_hide_alert_title diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 27418f3d01..f401253e1e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -29,9 +29,13 @@ internal class DefaultTokensListRouter : TokensListRouter { store.dispatchNotification(R.string.contract_address_copied_message) } - override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String) { + override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) { store.dispatchDialogShow( - dialog = AppDialog.TokensAreLinkedDialog(currencyTitle = tokenName, currencySymbol = tokenSymbol), + dialog = AppDialog.TokensAreLinkedDialog( + currencyTitle = tokenName, + currencySymbol = tokenSymbol, + networkName = networkName, + ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt index 34b5379b48..a11e584564 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt @@ -19,8 +19,9 @@ internal interface TokensListRouter { * * @param tokenName token name * @param tokenSymbol token brief name + * @param networkName blockchain network full name */ - fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String) + fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) /** * Open alert to remove wallet diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 53867da367..4874bc303c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -365,6 +365,7 @@ internal class TokensListViewModel @Inject constructor( router.openUnableHideMainTokenAlert( tokenName = blockchain.name, tokenSymbol = blockchain.currency, + networkName = blockchain.fullName, ) } else if (isAddedOnMainScreen) { router.openRemoveWalletAlert( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 076c60da41..34ae262e6d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -172,7 +172,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid title = resourceReference(id = R.string.main_swap_changelly_promotion_title), subtitle = resourceReference( id = R.string.main_swap_changelly_promotion_message, - formatArgs = wrappedList("1", "2"), + formatArgs = wrappedList("1", "2", "3"), ), iconResId = R.drawable.img_swap_promo, backgroundResId = R.drawable.img_swap_promo_blue_banner_background, @@ -180,7 +180,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid NotificationConfig( title = resourceReference( id = R.string.token_swap_changelly_promotion_title, - formatArgs = wrappedList("1", "2"), + formatArgs = wrappedList("1"), ), subtitle = stringReference( "Swap multiple currencies between any chains you wish. Swap multiple " + From 164124a7a5fa88c49eae2862b95780ad44727aab Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 17:06:59 +0400 Subject: [PATCH 05/15] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/navigation/NavigationReducer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt index ff53145651..1935d1f5d6 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt @@ -30,7 +30,7 @@ private fun internalReduce(action: Action, state: AppState): NavigationState { when { // Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a // new scan from the background [REDACTED_TASK_KEY] - navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null) + navState.activity?.get() == navigationAction.activity.get() -> NavigationState() else -> navState } } From 0a398ed270b1c0ef7fed791ba4c13204f3efb04d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 17:09:12 +0400 Subject: [PATCH 06/15] Updated on 2026-08-14 --- .../com/tangem/tap/data/DefaultCardSdkProvider.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index a164e0dde1..395f26e868 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -19,6 +19,8 @@ import com.tangem.sdk.extensions.* import com.tangem.sdk.nfc.NfcManager import com.tangem.sdk.storage.create import com.tangem.tap.foregroundActivityObserver +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @@ -30,6 +32,7 @@ import javax.inject.Singleton @Singleton internal class DefaultCardSdkProvider @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, ) : CardSdkProvider, CardSdkOwner { private val observer = Observer() @@ -39,12 +42,12 @@ internal class DefaultCardSdkProvider @Inject constructor( override val sdk: TangemSdk get() = holder?.sdk ?: tryToRegisterWithForegroundActivity() - override fun register(activity: FragmentActivity) { + override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) { if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) { val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing" analyticsEventHandler.send(TangemSdkWarningEvent(message)) Log.info { message } - return + return@runBlocking } if (holder != null) { @@ -58,7 +61,7 @@ internal class DefaultCardSdkProvider @Inject constructor( Log.info { "Tangem SDK owner registered" } } - private fun tryToRegisterWithForegroundActivity(): TangemSdk { + private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) { val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity" analyticsEventHandler.send(TangemSdkWarningEvent(warning)) Log.warning { warning } @@ -83,7 +86,7 @@ internal class DefaultCardSdkProvider @Inject constructor( error(error) } - return sdk + return@runBlocking sdk } private fun initialize(activity: FragmentActivity) { From 498690931fe415e9044c3c630d68695c2f009290 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 17:10:48 +0400 Subject: [PATCH 07/15] Updated on 2026-08-14 --- .../tangem/tap/domain/sdk/TangemSdkManager.kt | 4 ++ .../sdk/impl/DefaultTangemSdkManager.kt | 67 ++++++++++++++++++- .../domain/sdk/impl/MockTangemSdkManager.kt | 10 ++- .../DefaultLegacySettingsRepository.kt | 2 +- .../details/redux/DetailsMiddleware.kt | 6 +- .../features/details/redux/DetailsReducer.kt | 4 +- .../features/onboarding/OnboardingHelper.kt | 2 +- .../saveWallet/redux/SaveWalletMiddleware.kt | 38 +++++------ .../domain/settings/CanUseBiometryUseCase.kt | 2 +- .../repositories/LegacySettingsRepository.kt | 2 +- gradle/dependencies.toml | 2 +- 11 files changed, 107 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt index e3de1dc4a7..0f5e408f5e 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -33,6 +33,10 @@ interface TangemSdkManager { val userCodeRequestPolicy: UserCodeRequestPolicy + suspend fun checkCanUseBiometry(awaitInitialization: Boolean = true): Boolean + + suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean + suspend fun scanProduct( cardId: String? = null, messageRes: Int? = null, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 72b578cea9..00bacdbe26 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -3,9 +3,11 @@ package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import com.tangem.Log import com.tangem.Message import com.tangem.TangemSdk import com.tangem.common.* +import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.authentication.keystore.KeystoreManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* @@ -38,16 +40,21 @@ import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlin.coroutines.resume -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") class DefaultTangemSdkManager( private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources, ) : TangemSdkManager { + private val awaitInitializationMutex = Mutex() + private val tangemSdk: TangemSdk get() = cardSdkConfigRepository.sdk @@ -73,6 +80,42 @@ class DefaultTangemSdkManager( override val userCodeRequestPolicy: UserCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy + override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean { + return try { + needEnrollBiometrics + } catch (e: TangemSdkError.AuthenticationNotInitialized) { + Log.error { + "Trying to access `needEnrollBiometrics` flag when authentication manager is not initialized: " + + if (awaitInitialization) "awaiting initialization" else "failing" + } + + if (awaitInitialization) { + awaitAuthenticationManagerInitialization().needEnrollBiometrics + } else { + throw e + } + } + } + + override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean { + return try { + canUseBiometry + } catch (e: TangemSdkError.AuthenticationNotInitialized) { + Log.error { + "Trying to access `canUseBiometry` flag when authentication manager is not initialized: " + + if (awaitInitialization) "awaiting initialization" else "failing" + } + + if (awaitInitialization) { + val manager = awaitAuthenticationManagerInitialization() + + manager.canAuthenticate || manager.needEnrollBiometrics + } else { + throw e + } + } + } + override suspend fun scanProduct( cardId: String?, messageRes: Int?, @@ -288,6 +331,26 @@ class DefaultTangemSdkManager( tangemSdk.config.userCodeRequestPolicy = policy } + private suspend fun awaitAuthenticationManagerInitialization(): AuthenticationManager { + return awaitInitializationMutex.withLock { + var attemps = 0 + + do { + if (tangemSdk.authenticationManager.isInitialized) { + break + } else { + if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) { + error("Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts") + } else { + delay(timeMillis = 200) + } + } + } while (true) + + tangemSdk.authenticationManager + } + } + // region Twin-specific override suspend fun createFirstTwinWallet( @@ -335,6 +398,8 @@ class DefaultTangemSdkManager( // endregion companion object { + private const val MAX_INITIALIZE_ATTEMPTS = 10 + @Deprecated("Use [DefaultCardSdkProvider] instead") val config = Config( linkedTerminal = true, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 7c5585c68b..dcc850f798 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -28,9 +28,11 @@ class MockTangemSdkManager( private val resources: Resources, ) : TangemSdkManager { - override val canUseBiometry = false + private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default - override val needEnrollBiometrics = false + override val canUseBiometry: Boolean = false + + override val needEnrollBiometrics: Boolean = false override val keystoreManager = DummyKeystoreManager() @@ -39,7 +41,9 @@ class MockTangemSdkManager( override val userCodeRequestPolicy: UserCodeRequestPolicy get() = userCodeRequestPolicyInternal - private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default + override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean = canUseBiometry + + override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = needEnrollBiometrics override suspend fun scanProduct( cardId: String?, diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt index 00f7f53887..6a84f8cd30 100644 --- a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -7,5 +7,5 @@ internal class DefaultLegacySettingsRepository( private val tangemSdkManager: TangemSdkManager, ) : LegacySettingsRepository { - override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry + override suspend fun canUseBiometry(): Boolean = tangemSdkManager.checkCanUseBiometry() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index c82a82d087..ba234b662c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -37,7 +37,10 @@ import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.rekotlin.Action @@ -265,7 +268,6 @@ class DetailsMiddleware { .onEach { needEnrollBiometrics -> store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics)) } - .flowOn(Dispatchers.IO) .launchIn(lifecycleScope) .saveIn(checkBiometricsStatusJobHolder) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 5a915b21d5..68b87d064f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -76,7 +76,9 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS }, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appSettingsState = AppSettingsState( - isBiometricsAvailable = tangemSdkManager.canUseBiometry, + isBiometricsAvailable = runBlocking { + tangemSdkManager.checkCanUseBiometry() + }, saveWallets = action.shouldSaveUserWallets, saveAccessCodes = runBlocking { store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index f03dc73425..3aca958d05 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -99,7 +99,7 @@ object OnboardingHelper { } // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen - tangemSdkManager.canUseBiometry && settingsRepository.shouldShowSaveUserWalletScreen() -> { + tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> { proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) delay(timeMillis = 1_200) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 45eb5225a2..4069551f22 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -91,10 +91,10 @@ internal class SaveWalletMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } - private fun allowToUseBiometrics(state: SaveWalletState) { - if (tangemSdkManager.needEnrollBiometrics) { - store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) - return + private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch { + if (tangemSdkManager.checkNeedEnrollBiometrics()) { + store.dispatchWithMain(SaveWalletAction.EnrollBiometrics) + return@launch } if (state.backupInfo != null) { @@ -104,24 +104,22 @@ internal class SaveWalletMiddleware { Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) } - scope.launch { - /* + /* - * because it will be automatically saved on UserWalletsListManager switch - */ - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - val error = IllegalStateException("No selected user wallet") - Timber.e(error, "Unable to save user wallet") - store.dispatchWithMain( - SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)), - ) - return@launch - } + * because it will be automatically saved on UserWalletsListManager switch + */ + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + val error = IllegalStateException("No selected user wallet") + Timber.e(error, "Unable to save user wallet") + store.dispatchWithMain( + SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)), + ) + return@launch + } - handleSuccessAllowing(selectedUserWallet) - }.saveIn(saveWalletJobHolder) - } + handleSuccessAllowing(selectedUserWallet) + }.saveIn(saveWalletJobHolder) private suspend fun handleSuccessAllowing(userWallet: UserWallet) { store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt index 390c70dcc4..880f941db8 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt @@ -4,5 +4,5 @@ import com.tangem.domain.settings.repositories.LegacySettingsRepository class CanUseBiometryUseCase(private val legacySettingsRepository: LegacySettingsRepository) { - operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry() + suspend operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt index 9e1dc7cf3a..e1f1b8ebb7 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt @@ -2,5 +2,5 @@ package com.tangem.domain.settings.repositories interface LegacySettingsRepository { - fun canUseBiometry(): Boolean + suspend fun canUseBiometry(): Boolean } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 41817d438f..b7ef29922c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "release-app_5.12-698" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.12-369" +tangemCardSdk = "release-app_5.12-371" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From 51ce614b7b77691ecad62e146dd1a09128c6279d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 16:49:16 +0100 Subject: [PATCH 08/15] Updated on 2026-08-14 --- .../data/tokens/paging/CoinsPagingSource.kt | 26 +++++++++++-------- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++++++- .../viewmodels/ExchangeStatusFactory.kt | 11 +++++++- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt index e5c4c36e1a..9cf7b0092c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt @@ -52,19 +52,23 @@ internal class CoinsPagingSource( suffix = CryptoCurrency.ID.Suffix.RawID(coin.id), ) } - val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false) - LoadResult.Page( - data = CoinsResponseConverter.convert( - CoinsData( - response.coins, - response.imageHost, - quotes, + try { + val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false) + LoadResult.Page( + data = CoinsResponseConverter.convert( + CoinsData( + response.coins, + response.imageHost, + quotes, + ), ), - ), - prevKey = if (page == 0) null else page.minus(other = 1), - nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), - ) + prevKey = if (page == 0) null else page.minus(other = 1), + nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), + ) + } catch (t: Throwable) { + LoadResult.Error(t) + } }, onFailure = { LoadResult.Error(it) }, ) 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 df53e5e0e7..99a1b3e2b9 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 @@ -1814,13 +1814,21 @@ internal class SwapInteractorImpl @Inject constructor( } private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { - val set = quotesRepository.getQuotesSync(ids.toSet(), false) + val set = ids.toSet().getQuotesOrEmpty(false) return ids .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } .toMap() } + private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { + return try { + quotesRepository.getQuotesSync(this, refresh) + } catch (t: Throwable) { + emptySet() + } + } + private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees { val demoFee = ProxyAmount( currencySymbol = cryptoCurrency.symbol, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index e77952c869..fb15bd476d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -70,7 +70,8 @@ internal class ExchangeStatusFactory( .map { savedTransactions -> val quotes = savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } - ?.let { quotesRepository.getQuotesSync(it.toSet(), true) } + ?.toSet() + ?.getQuotesOrEmpty(true) ?: emptySet() getExchangeStatusState( @@ -176,4 +177,12 @@ internal class ExchangeStatusFactory( else -> null } } + + private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { + return try { + quotesRepository.getQuotesSync(this, refresh) + } catch (t: Throwable) { + emptySet() + } + } } \ No newline at end of file From 0c21b7571fa4f7a9e366b565edfe8e40368304e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 11:48:23 +0100 Subject: [PATCH 09/15] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b7ef29922c..d55c28c4fa 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "release-app_5.12-698" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.12-371" +tangemCardSdk = "release-app_5.12-373" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From b209629c5d72f695285bc2ef0a5fbe03c226b66d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 17:41:38 +0500 Subject: [PATCH 10/15] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d55c28c4fa..e5f1b0cf1f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-698" +tangemBlockchainSdk = "release-app_5.12-701" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.12-373" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 26b5ea8a91ef260443d21f1bb09431b06bafcf6d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 18:15:23 +0500 Subject: [PATCH 11/15] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 5 ++ .../GeneralUserWalletsListManager.kt | 28 +++++++--- .../app/WalletConnectEventsHandlerImpl.kt | 4 ++ .../DefaultLegacyWalletConnectRepository.kt | 15 +++++- .../di/WalletConnectInteractorModule.kt | 13 +++-- .../domain/WalletConnectEventsHandler.kt | 2 + .../domain/WalletConnectInteractor.kt | 53 ++++++++++++++++--- .../domain/models/WalletConnectEvents.kt | 2 + .../walletconnect/WalletConnectAction.kt | 10 ++-- .../walletconnect/WalletConnectMiddleware.kt | 10 +++- .../walletconnect/WalletConnectReducer.kt | 1 + .../redux/walletconnect/WalletConnectState.kt | 2 + 12 files changed, 116 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 01625dc4f2..4352a65eb9 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -126,6 +126,11 @@ class DialogManager : StoreSubscriber { preparedData = state.dialog.data, context = context, ) + is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create( + titleRes = R.string.wallet_connect_title, + message = state.dialog.error.message, + context = context, + ) is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 4565c111ed..2901b7120b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -48,21 +48,33 @@ internal class GeneralUserWalletsListManager( get() = requireImplementation.isLockable override val userWallets: Flow> - get() = implementation.transformLatest { impl -> - if (impl != null && impl.hasUserWallets) { - emitAll(impl.userWallets) + get() = implementation + .transformLatest { impl -> + if (impl != null) { + emitAll(impl.userWallets) + } } - } + // To avoid returning empty flow to subscriber while implementation and userWallets are null + // Flow is called first time when implementation is null and then when its assigned with implementation + // that may have not user wallets (null or empty). + // As a result subscription occurs on empty flow, than will not change if user wallets are available + .filter { requireImplementation.hasUserWallets } override val userWalletsSync: List get() = requireImplementation.userWalletsSync override val selectedUserWallet: Flow - get() = implementation.transformLatest { impl -> - if (impl != null && impl.hasUserWallets) { - emitAll(impl.selectedUserWallet) + get() = implementation + .transformLatest { impl -> + if (impl != null) { + emitAll(impl.selectedUserWallet) + } } - } + // To avoid returning empty flow to subscriber while implementation and userWallets are null + // Flow is called first time when implementation is null and then when its assigned with implementation + // that may have not user wallets (null or empty). + // As a result subscription occurs on empty flow, than will not change if user wallets are available + .filter { requireImplementation.hasUserWallets } override val selectedUserWalletSync: UserWallet? get() = requireImplementation.selectedUserWalletSync diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt index 909f4dafb7..073fcb8f0d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt @@ -46,4 +46,8 @@ internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { override fun onUnsupportedRequest() { store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest) } + + override fun onPairConnectError(error: Throwable) { + store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 922581137c..acee05571c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -246,7 +246,20 @@ internal class DefaultLegacyWalletConnectRepository( } override fun pair(uri: String) { - Web3Wallet.pair(Wallet.Params.Pair(uri)) + Web3Wallet.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { + Timber.i("Paired successfully: $it") + }, + onError = { + Timber.e("Error while pairing: $it") + scope.launch { + _events.emit( + WalletConnectEvents.PairConnectError(it.throwable), + ) + } + }, + ) } override fun approve(userNamespaces: Map>) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index d4fef91bab..cc4a3e16c4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -14,26 +14,24 @@ import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object WalletConnectInteractorModule { @Provides - @ActivityScoped + @Singleton fun provideWalletConnectInteractor( wcRepository: LegacyWalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, @@ -41,6 +39,7 @@ internal object WalletConnectInteractorModule { currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, + coroutineDispatcherProvider: CoroutineDispatcherProvider, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -51,7 +50,7 @@ internal object WalletConnectInteractorModule { currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, - dispatchers = AppCoroutineDispatcherProvider(), + dispatchers = coroutineDispatcherProvider, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt index 30c73a0642..b9414d247e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt @@ -16,4 +16,6 @@ interface WalletConnectEventsHandler { fun onSessionRequest(request: WcPreparedRequest) fun onUnsupportedRequest() + + fun onPairConnectError(error: Throwable) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index b63a1d0005..66f8b5d16c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -9,18 +9,18 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Stack @Suppress("LargeClass", "LongParameterList") class WalletConnectInteractor( @@ -35,18 +35,27 @@ class WalletConnectInteractor( val blockchainHelper: WcBlockchainHelper, ) { + var isWalletConnectReadyForDeepLinks = false + private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedWalletUseCase(userWalletsListManager) } private val wcScope = CoroutineScope( - Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"), + SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> + Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") + }, ) private val listenerScope = CoroutineScope( - Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"), + SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> + Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") + }, ) + /** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */ + private val deeplinkStack: Stack = Stack() + private val events = walletConnectRepository.events private val sessions = walletConnectRepository.activeSessions @@ -96,6 +105,7 @@ class WalletConnectInteractor( networks = currencies.map { it.network }, ) setUserChains(accounts) + handleDeeplinkStack(accounts) } private suspend fun startListeningWc(userWalletId: String, cardId: String?) { @@ -118,6 +128,17 @@ class WalletConnectInteractor( walletConnectRepository.setUserNamespaces(userNamespaces) } + private fun handleDeeplinkStack(accounts: List) { + runCatching { + if (accounts.isEmpty()) return + isWalletConnectReadyForDeepLinks = true + val lastDeeplink = deeplinkStack.pop() + store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) + }.onFailure { + Timber.e("WC deeplink handling failed. $it") + } + } + private suspend fun subscribeToEvents() { events .onEach { wcEvent -> @@ -167,6 +188,9 @@ class WalletConnectInteractor( is WalletConnectEvents.SessionRequest -> { handleRequest(wcEvent) } + is WalletConnectEvents.PairConnectError -> { + handler.onPairConnectError(wcEvent.error) + } } } .flowOn(dispatchers.io) @@ -329,6 +353,21 @@ class WalletConnectInteractor( return uri.lowercase().startsWith(WC_SCHEME) } + /** + * Handles Wallet Connect deep links. + * If wallet connect is able to handle the deeplink, session is started with deeplink. + * Otherwise, deeplink is stored until wallet connect is ready to handle it. + * + * @param deeplink deeplink to handle + */ + fun addDeeplink(deeplink: String) { + if (isWalletConnectReadyForDeepLinks) { + store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) + } else { + deeplinkStack.push(deeplink) + } + } + private fun getCardId(userWallet: UserWallet): String? { return if (userWallet.scanResponse.card.backupStatus?.isActive != true) { userWallet.cardId diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt index d47c296a42..85e6e06f5b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -26,4 +26,6 @@ sealed interface WalletConnectEvents { val metaUrl: String, val method: String, ) : WalletConnectEvents + + data class PairConnectError(val error: Throwable) : WalletConnectEvents } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index e96974c3b4..a6c5dabb19 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -22,17 +22,19 @@ sealed class WalletConnectAction : Action { data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() //region WalletConnect 2.0 - object ApproveProposal : WalletConnectAction() - object RejectProposal : WalletConnectAction() + data object ApproveProposal : WalletConnectAction() + data object RejectProposal : WalletConnectAction() - object SessionEstablished : WalletConnectAction() + data object SessionEstablished : WalletConnectAction() data class SessionRejected(val error: WalletConnectError) : WalletConnectAction() data class SessionListUpdated(val sessions: List) : WalletConnectAction() data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction() - object RejectUnsupportedRequest : WalletConnectAction() + data object RejectUnsupportedRequest : WalletConnectAction() data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction() + + data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction() //endregion WalletConnect 2.0 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index b4917ceabb..3afa643211 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -49,8 +49,11 @@ class WalletConnectMiddleware { when (action) { is WalletConnectAction.HandleDeepLink -> { - if (!action.wcUri.isNullOrBlank()) { - store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) + val wsUrl = action.wcUri + Timber.i("WC deeplink: $wsUrl") + if (!wsUrl.isNullOrBlank()) { + Timber.i("WC deeplink added to stack: $wsUrl") + walletConnectInteractor.addDeeplink(wsUrl) } } is WalletConnectAction.DisconnectSession -> { @@ -181,6 +184,9 @@ class WalletConnectMiddleware { is WalletConnectAction.RejectUnsupportedRequest -> { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) } + is WalletConnectAction.PairConnectErrorAction -> { + store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable))) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index 20217386f2..a7640ed758 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -14,6 +14,7 @@ object WalletConnectReducer { is WalletConnectAction.RejectProposal, is WalletConnectAction.SessionEstablished, is WalletConnectAction.SessionRejected, + is WalletConnectAction.PairConnectErrorAction, -> state.copy(loading = false) is WalletConnectAction.SessionListUpdated -> state.copy( wc2Sessions = action.sessions, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 750f4b04c5..f407924cca 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -117,6 +117,8 @@ sealed class WalletConnectDialog : StateDialog { data class SignTransactionDialog( val data: WcPreparedRequest.SignTransaction, ) : WalletConnectDialog() + + data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog() } data class WcTransactionData( From a670ab02aac71c9bec54f8d457e856d5c84f62b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 18:08:16 +0100 Subject: [PATCH 12/15] Updated on 2026-08-14 --- .../tap/common/redux/legacy/LegacyMiddleware.kt | 4 ++++ .../wallet/redux/OnboardingWalletMiddleware.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 4 ++-- core/res/src/main/res/values/strings.xml | 2 +- .../java/com/tangem/domain/redux/LegacyAction.kt | 4 +++- .../presentation/wallet/domain/BackupValidator.kt | 6 +++++- .../wallet/domain/GetMultiWalletWarningsFactory.kt | 11 +++++++---- .../wallet/state/model/WalletNotification.kt | 13 +++++++++++-- .../intents/WalletWarningsClickIntents.kt | 8 +++++++- 9 files changed, 41 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 7cf0e08bf1..4b7bdcdf15 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.domain.redux.LegacyAction import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState @@ -21,6 +22,9 @@ internal object LegacyMiddleware { is LegacyAction.SendEmailRateCanBeBetter -> { store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail()) } + is LegacyAction.SendEmailSupport -> { + store.state.globalState.feedbackManager?.sendEmail(FeedbackEmail()) + } is LegacyAction.StartOnboardingProcess -> { store.dispatch( GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 925dc9bcc2..2bf1bc1b5e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -483,7 +483,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) when (result) { is CompletionResult.Success -> { val backupValidator = BackupValidator() - if (!backupValidator.isValid(CardDTO(result.data))) { + if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) { store.dispatchOnMain(BackupAction.ErrorInBackupCard) } if (backupService.currentState == BackupService.State.Finished) { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e70467d63f..ae139741ff 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -397,7 +397,7 @@ Код доступа Группы По балансу - Сортировка токенов + Упорядочить токены Список Выбрать из галереи Настройки @@ -670,7 +670,7 @@ Настройки кошелька Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку - Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. + Похоже, что процесс активации карт не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей. Ошибка активации По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. Отключение сети BNB Beacon Chain diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 40b0b45e01..442b69ba9b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -663,7 +663,7 @@ Wallet settings Tangem Use %s or scan a card to unlock access to your wallet - Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. + It seems that the card activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card to your device. Please contact our Support team for assistance. Activation error According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. BNB Beacon Chain will shut down diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index 3e0b022afd..eb116384ba 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -8,7 +8,9 @@ import java.math.BigDecimal sealed interface LegacyAction : Action { - object SendEmailRateCanBeBetter : LegacyAction + data object SendEmailSupport : LegacyAction + + data object SendEmailRateCanBeBetter : LegacyAction /** * Initiate an onboarding process. diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt index 4153d69e68..d83e759503 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt @@ -7,10 +7,14 @@ import javax.inject.Inject class BackupValidator @Inject constructor() { - fun isValid(cardDTO: CardDTO): Boolean { + fun isValidFull(cardDTO: CardDTO): Boolean { return validateBackupStatus(cardDTO) && validateCurves(cardDTO) } + fun isValidBackupStatus(cardDTO: CardDTO): Boolean { + return validateBackupStatus(cardDTO) + } + private fun validateCurves(cardDTO: CardDTO): Boolean { val config = CardConfig.createConfig(cardDTO) // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, 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 b0513c7656..ef7f55da6d 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 @@ -58,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( buildList { addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) - addCriticalNotifications(userWallet) + addCriticalNotifications(userWallet, clickIntents) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -86,11 +86,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addCriticalNotifications(userWallet: UserWallet) { + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + clickIntents: WalletClickIntents, + ) { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( - element = WalletNotification.Critical.BackupError, - condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError, + element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, ) addIf( 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 4a3247b5e1..258a9f73f9 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 @@ -20,11 +20,16 @@ import org.joda.time.DateTime @Immutable sealed class WalletNotification(val config: NotificationConfig) { - sealed class Critical(title: TextReference, subtitle: TextReference) : WalletNotification( + sealed class Critical( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + ) : WalletNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, ), ) { @@ -38,9 +43,13 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), ) - data object BackupError : Critical( + data class BackupError(val onSupportClick: () -> Unit) : Critical( title = resourceReference(R.string.warning_backup_errors_title), subtitle = resourceReference(R.string.warning_backup_errors_message), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(id = R.string.details_row_title_contact_to_support), + onClick = onSupportClick, + ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index d2af11e81b..d7e79ce743 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -62,6 +62,8 @@ internal interface WalletWarningsClickIntents { fun onTravalaPromoClick(link: String?) fun onCloseTravalaPromoClick() + + fun onSupportClick() } @Suppress("LongParameterList") @@ -254,7 +256,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - private suspend fun getSelectedUserWallet(): UserWallet? { + override fun onSupportClick() { + reduxStateHolder.dispatch(LegacyAction.SendEmailSupport) + } + + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { Timber.e( From 62545c47e86fcca96e68dd7d799b995a6be599b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jul 2024 12:04:40 +0300 Subject: [PATCH 13/15] Updated on 2026-08-14 --- .../tangem/core/ui/components/transactions/TransactionList.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 6b4cc1bd3d..220b5c82ca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -82,7 +82,9 @@ private fun LazyListScope.contentItems( when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.itemKey is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() - is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + is TxHistoryState.TxHistoryItemState.Transaction -> + item.state.txHash + + ((item.state as? TransactionState.Content)?.hashCode() ?: "") } }, contentType = txHistoryItems.itemContentType { it::class.java }, From e6d7bee87a5cd50c03570e06dd19394b4ae6876b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 13:21:11 +0500 Subject: [PATCH 14/15] Updated on 2026-08-14 --- .../DefaultLegacyWalletConnectRepository.kt | 3 ++- .../domain/LegacyWalletConnectRepository.kt | 2 ++ .../domain/WalletConnectInteractor.kt | 20 +++++++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index acee05571c..4f9055bfb4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -35,7 +35,8 @@ internal class DefaultLegacyWalletConnectRepository( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions - private var currentSessions: List = emptyList() + override var currentSessions: List = emptyList() + private set /** * @param projectId Project ID at https://cloud.walletconnect.com/ diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 9096e879e0..616643a960 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -9,6 +9,8 @@ interface LegacyWalletConnectRepository { val activeSessions: Flow> + val currentSessions: List + fun init(projectId: String) fun setUserNamespaces(userNamespaces: Map>) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 66f8b5d16c..ae9d50d6cf 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain +import android.net.Uri import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency @@ -132,6 +133,7 @@ class WalletConnectInteractor( runCatching { if (accounts.isEmpty()) return isWalletConnectReadyForDeepLinks = true + if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) }.onFailure { @@ -361,6 +363,18 @@ class WalletConnectInteractor( * @param deeplink deeplink to handle */ fun addDeeplink(deeplink: String) { + val deeplinkQueries = deeplink.split(WC_SPLIT_CHAR).lastOrNull() + val sessionTopic = deeplinkQueries?.let { Uri.parse(it).getQueryParameter(WC_TOPIC_QUERY_NAME) } + + val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session -> + session.topic == sessionTopic + } + + if (isAlreadyActiveSessionTopic && deeplinkQueries != null) { + Timber.i("WC already has an active session topic: $deeplink") + return + } + if (isWalletConnectReadyForDeepLinks) { store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) } else { @@ -406,7 +420,9 @@ class WalletConnectInteractor( return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } - companion object { - private const val WC_SCHEME = "wc" + private companion object { + const val WC_SCHEME = "wc" + const val WC_TOPIC_QUERY_NAME = "sessionTopic" + const val WC_SPLIT_CHAR = "/" } } \ No newline at end of file From ec7b018eedd48080bfcfd363790bce468c177748 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 16:27:33 +0500 Subject: [PATCH 15/15] Updated on 2026-08-14 --- .../walletconnect2/domain/WalletConnectInteractor.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index ae9d50d6cf..1f5dce9663 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.walletconnect2.domain -import android.net.Uri import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency @@ -363,14 +362,15 @@ class WalletConnectInteractor( * @param deeplink deeplink to handle */ fun addDeeplink(deeplink: String) { - val deeplinkQueries = deeplink.split(WC_SPLIT_CHAR).lastOrNull() - val sessionTopic = deeplinkQueries?.let { Uri.parse(it).getQueryParameter(WC_TOPIC_QUERY_NAME) } + val deeplinkRegex = Regex(WC_PARAM_REGEX) + val matched = deeplinkRegex.findAll(deeplink) + val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull() val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session -> session.topic == sessionTopic } - if (isAlreadyActiveSessionTopic && deeplinkQueries != null) { + if (isAlreadyActiveSessionTopic && sessionTopic != null) { Timber.i("WC already has an active session topic: $deeplink") return } @@ -423,6 +423,6 @@ class WalletConnectInteractor( private companion object { const val WC_SCHEME = "wc" const val WC_TOPIC_QUERY_NAME = "sessionTopic" - const val WC_SPLIT_CHAR = "/" + const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)" } } \ No newline at end of file