diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 580686d592..f234d867d6 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -42,6 +42,7 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer @@ -148,4 +149,6 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory + + fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 8977b105fc..5dd5a03d4c 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -60,6 +60,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.tap.common.analytics.AnalyticsFactory +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -235,6 +236,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() + private val customerIoFeatureToggles: CustomerIoFeatureToggles + get() = entryPoint.getCustomerIoFeatureToggles() + // endregion private val appScope = MainScope() @@ -406,7 +410,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory)) - factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) + + if (customerIoFeatureToggles.isFeatureEnabled) { + factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) + } factory.addFilter(oneTimeEventFilter) factory.addFilter(AppsFlyerEventFilter()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt new file mode 100644 index 0000000000..ae70817e3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +class CustomerIoFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "CUSTOMER_IO_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt index 834244e2ec..ddd534e938 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -4,6 +4,7 @@ import android.app.Application import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder +import io.customer.sdk.data.model.Region import timber.log.Timber /** @@ -25,6 +26,7 @@ internal class CustomerIoClient( applicationContext = application, cdpApiKey = cdpApiKey, ) + .region(Region.EU) .trackApplicationLifecycleEvents(false) .autoTrackActivityScreens(false) .addCustomerIOModule(ModuleMessagingPushFCM()) diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 7c584a3bd6..714a4acfd7 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -3,12 +3,19 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles +import dagger.hilt.android.AndroidEntryPoint import io.customer.messagingpush.CustomerIOFirebaseMessagingService import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint @SuppressLint("MissingFirebaseInstanceTokenRefresh") internal class TangemPushNotificationService : FirebaseMessagingService() { + @Inject + lateinit var customerIoFeatureToggles: CustomerIoFeatureToggles + private val pushNotificationDelegate: PushNotificationDelegate by lazy { PushNotificationDelegate(applicationContext) } @@ -17,13 +24,17 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { super.onNewToken(token) Timber.d("New FCM token received: $token") - CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) + } } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) - CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + } val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID 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 784db9dbc2..9918edc9f1 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -168,7 +168,7 @@ internal class DefaultCardSdkProvider @Inject constructor( secureStorage = secureStorage, authenticationManager = authenticationManager, keystoreManager = keystoreManager, - wordlist = Wordlist.getWordlist(activity), + wordlist = Wordlist.getWordlist(), config = config.apply { val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech) tangemApiBaseUrl = apiConfig.baseUrl diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 0d9a4eba92..595d7ccdcb 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -62,23 +62,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore - .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) - } + appPreferencesStore + .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) } override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), + ) + .takeIf { !it.isNullOrEmpty() } } override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - } + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") } override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) = @@ -92,58 +88,57 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? { - val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(tokensAdapter::fromJson) + return withContext(dispatcherProvider.io) { + val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) - return authTokens?.let { tokens -> - if (tokens.idempotencyKey == null) { - val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) - storeAuthTokens(customerWalletAddress, newAuthTokens) - newAuthTokens - } else { - tokens + authTokens?.let { tokens -> + if (tokens.idempotencyKey == null) { + val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) + storeAuthTokens(customerWalletAddress, newAuthTokens) + newAuthTokens + } else { + tokens + } } } } override suspend fun clearAuthTokens(customerWalletAddress: String) { - secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + withContext(dispatcherProvider.io) { + secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + } } override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) } override suspend fun getOrderId(customerWalletAddress: String): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) + .takeIf { !it.isNullOrEmpty() } } override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), + ) == true } override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) } - override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + override suspend fun clearOrderId(customerWalletAddress: String) { appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") } override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), isPaeraCustomer) + appPreferencesStore.store( + PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), + isPaeraCustomer, + ) } override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? { @@ -152,7 +147,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -179,7 +176,9 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = appPreferencesStore.getObjectMapSync( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) return orders[createWithdrawOrderIdKey(userWalletId)] } @@ -191,7 +190,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -221,40 +222,33 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) } override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), + ) == true } override suspend fun storeTangemPayEligibility(eligibility: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) - } + appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) } override suspend fun getTangemPayEligibility(): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) - } + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) } - override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) } + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) + } private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt index 64554a30e0..1830c50c92 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt @@ -1,31 +1,11 @@ package com.tangem.common -import com.tangem.utils.SupportedLanguages.CHINESE -import com.tangem.utils.SupportedLanguages.ENGLISH -import com.tangem.utils.SupportedLanguages.FRANCH -import com.tangem.utils.SupportedLanguages.GERMAN -import com.tangem.utils.SupportedLanguages.JAPANESE -import java.util.Locale - object TangemSiteShareUrlBuilder { private const val BASE_URL = "https://tangem.com" private const val CRYPTOCURRENCIES_PATH = "cryptocurrencies" - @Deprecated("Should use CHINESE from SupportedLanguages, but the site expects zh-Hans in the URL path") - private const val CHINESE_SITE_LOCALE = "zh-Hans" - - @Deprecated("Should rely on SupportedLanguages instead of maintaining a separate list") - private val siteLocales = mapOf( - ENGLISH to ENGLISH, - FRANCH to FRANCH, - GERMAN to GERMAN, - JAPANESE to JAPANESE, - CHINESE to CHINESE_SITE_LOCALE, - ) - fun shareUrl(tokenId: String): String { - val locale = siteLocales[Locale.getDefault().language] ?: ENGLISH - return "$BASE_URL/$locale/$CRYPTOCURRENCIES_PATH/$tokenId" + return "$BASE_URL/$CRYPTOCURRENCIES_PATH/$tokenId" } } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 46aea1e987..0d79f7bdf7 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -34,11 +34,11 @@ }, { "name": "EARN_BLOCK_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "HOLD_TO_CONFIRM_BUTTON_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "WALLET_REORDER_FEATURE_ENABLED", @@ -56,6 +56,10 @@ "name": "MULTI_ADDRESS_UTXO_ENABLED", "version": "undefined" }, + { + "name": "CUSTOMER_IO_ENABLED", + "version": "5.35" + }, { "name": "MAIN_SCREEN_QR_SCANNING_ENABLED", "version": "undefined" diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt index 2bab1747cf..9325df9a70 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -3,10 +3,13 @@ package com.tangem.datasource.asset.loader import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.adapter -import com.tangem.utils.coroutines.runCatching +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.NetworkMoshi import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -25,25 +28,44 @@ import javax.inject.Singleton class AssetLoader @Inject constructor( val assetReader: AssetReader, @NetworkMoshi val moshi: Moshi, + val analyticsExceptionHandler: AnalyticsExceptionHandler, val dispatchers: CoroutineDispatcherProvider, ) { /** Load content [Content] of asset file [fileName] */ + @Suppress("SuspendFunSwallowedCancellation") @OptIn(ExperimentalStdlibApi::class) - suspend inline fun load(fileName: String): Content? = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun load(fileName: String): Content? = withContext(dispatchers.io) { + val json = runCatching { assetReader.read(fullFileName = "$fileName.json") }.getOrNull() - moshi.adapter().fromJson(json) + runCatching { + moshi.adapter().fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) { + sendException( + fileName = fileName, + isParsingSuccess = true, + json = json, + ) + + Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + } + + parsedConfig + }, + onFailure = { throwable -> + sendException( + fileName = fileName, + isParsingSuccess = false, + json = json, + ) + + Timber.e(throwable, "Failed to load config [$fileName] from assets") + null + }, + ) } - .fold( - onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) - parsedConfig - }, - onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") - null - }, - ) /** Load list [V] values of asset file [fileName] */ suspend inline fun loadList(fileName: String): List = runCatching(dispatchers.io) { @@ -84,4 +106,18 @@ class AssetLoader @Inject constructor( emptyMap() }, ) + + fun sendException(fileName: String, isParsingSuccess: Boolean, json: String?) { + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException("Parsing config is failed"), + params = mapOf( + "filename" to fileName, + "isParsingSuccess" to isParsingSuccess.toString(), + "json_size" to json?.length.toString(), + "json" to json?.take(n = 30).toString(), + ), + ), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt index 3847d29980..b2c9c5b2e2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -1,7 +1,6 @@ package com.tangem.datasource.asset.reader import android.content.res.AssetManager -import java.io.BufferedReader /** * Implementation of asset file reader @@ -13,7 +12,8 @@ internal class AndroidAssetReader( ) : AssetReader { override suspend fun read(fullFileName: String): String { - return assetManager.open(fullFileName).bufferedReader() - .use(BufferedReader::readText) + return assetManager.open(fullFileName, AssetManager.ACCESS_BUFFER).use { inputStream -> + inputStream.readBytes().toString(Charsets.UTF_8) + } } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt index 1bbee64573..a1594149d3 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.adapter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.asset.reader.AssetReader import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -23,9 +24,11 @@ class AssetLoaderTest { private val assetReader = mockk() private val moshi = mockk() + private val analyticsExceptionHandler = mockk() private val assetLoader = AssetLoader( assetReader = assetReader, moshi = moshi, + analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt index 651d234c8d..93425e2814 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt @@ -18,7 +18,7 @@ internal class AndroidAssetReaderTest { @Test fun read_content() = runTest { - every { assetManager.open(FILE_NAME) } returns json.byteInputStream() + every { assetManager.open(FILE_NAME, AssetManager.ACCESS_BUFFER) } returns json.byteInputStream() val actual = assetReader.read(fullFileName = FILE_NAME) @@ -28,7 +28,7 @@ internal class AndroidAssetReaderTest { @Test fun read_error() = runTest { val exception = IOException("Error") - every { assetManager.open(FILE_NAME) } throws exception + every { assetManager.open(FILE_NAME, AssetManager.ACCESS_BUFFER) } throws exception runCatching { assetReader.read(fullFileName = FILE_NAME) } .onSuccess { throw IllegalStateException("Error should be thrown") } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index c88fd2fd38..555c6c8e32 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest import com.tangem.datasource.api.pay.models.request.WithdrawRequest import com.tangem.datasource.api.pay.models.response.WithdrawResponse import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayWithdrawExchangeState @@ -23,8 +22,12 @@ import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addHexPrefix -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -37,6 +40,9 @@ import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds private const val TAG = "TangemPaySwapRepository" +private const val MAX_POLLING_ATTEMPTS = 6 + +private data class PollingKey(val userWalletId: String, val orderId: String) @Suppress("LongParameterList") internal class DefaultTangemPayWithdrawRepository @Inject constructor( @@ -50,8 +56,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private val withdrawPollingScope: AppCoroutineScope, ) : TangemPayWithdrawRepository { - private val withdrawPollingJobs = mutableMapOf() - private val withdrawPollingMutex = Mutex() + private val pollingJobs = mutableMapOf() + private val pollingMutex = Mutex() override suspend fun withdraw( userWallet: UserWallet, @@ -122,6 +128,76 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + startPollingForOrder(userWallet, orderId, exchangeData) + } + } + } + + private fun startPollingForOrder( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + withdrawPollingScope.launch { + startWithdrawOrderPolling(userWallet, orderId, exchangeData) + } + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val key = PollingKey(userWallet.walletId.stringValue, orderId) + + pollingMutex.withLock { + if (pollingJobs.containsKey(key)) return@withLock + + val pollingJob = withdrawPollingScope.launch { + try { + var attemptCount = 0 + while (isActive && attemptCount < MAX_POLLING_ATTEMPTS) { + delay(duration = 3.seconds) + attemptCount++ + val result = orderRepository.getOrderData( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + val txHash = result.getOrNull()?.withdrawTxHash?.ifEmpty { null } + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + txHash = txHash, + exchangeData = exchangeData, + orderId = orderId, + ) + pollingMutex.withLock { pollingJobs.remove(key) } + return@launch + } + } + if (attemptCount >= MAX_POLLING_ATTEMPTS) { + Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } + pollingJobs[key] = pollingJob + } + } + + private fun stopPolling(userWalletId: String, orderId: String) { + withdrawPollingScope.launch { + pollingMutex.withLock { + val key = PollingKey(userWalletId, orderId) + pollingJobs[key]?.cancel() + pollingJobs.remove(key) } } } @@ -141,7 +217,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( txHash = txHash, payInExtraId = exchangeData.payInExtraId, ).also { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + stopPolling(userWalletId = userWallet.walletId.stringValue, orderId = orderId) } } @@ -158,14 +235,12 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> - withdrawPollingScope.launch { - try { - pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) - } catch (exception: CancellationException) { - throw exception - } catch (exception: Exception) { - Timber.tag(TAG).e(exception) - } + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) } } } @@ -190,51 +265,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( if (!txHash.isNullOrEmpty()) { finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData, orderId = orderId) } else { - startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) - } - return - } - - private suspend fun startWithdrawOrderPolling( - userWallet: UserWallet, - orderId: String, - exchangeData: TangemPayWithdrawExchangeState, - ) { - withdrawPollingMutex.withLock { - if (withdrawPollingJobs.containsKey(orderId)) return - - val pollingJob = withdrawPollingScope.launch { - try { - while (isActive) { - delay(duration = 5.seconds) - - orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) - .onRight { order -> - val txHash = order.withdrawTxHash - if (txHash.isNullOrEmpty()) return@onRight - finalizeWithdraw( - userWallet = userWallet, - txHash = txHash, - exchangeData = exchangeData, - orderId = orderId, - ) - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } - .onLeft { error -> - Timber.tag(TAG).e("getOrderData error ${error.errorCode}") - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } - } - } catch (exception: CancellationException) { - throw exception - } catch (exception: Exception) { - Timber.tag(TAG).e(exception) - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - } - } - withdrawPollingJobs[orderId] = pollingJob + startPollingForOrder(userWallet, orderId, exchangeData) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 8a0c2b7d45..dc5c0bb1ba 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -21,7 +21,6 @@ import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier -import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService @@ -92,6 +91,7 @@ internal object WalletConnectDataModule { dispatchers: CoroutineDispatcherProvider, getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, + multiAccountListSupplier: MultiAccountListSupplier, analytics: AnalyticsEventHandler, appScope: AppCoroutineScope, ): DefaultWcSessionsManager { @@ -102,6 +102,7 @@ internal object WalletConnectDataModule { wcNetworksConverter = wcNetworksConverter, analytics = analytics, scope = appScope, + multiAccountListSupplier = multiAccountListSupplier, ) } @@ -177,12 +178,10 @@ internal object WalletConnectDataModule { namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - singleAccountSupplier: SingleAccountSupplier, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, singleAccountStatusListSupplier = singleAccountStatusListSupplier, - singleAccountSupplier = singleAccountSupplier, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 0d9625fc6f..d27195854e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -6,10 +6,16 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.data.walletconnect.utils.* +import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.data.walletconnect.utils.WcNetworksConverter +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO @@ -29,7 +35,8 @@ import kotlin.coroutines.resume @Suppress("LongParameterList") internal class DefaultWcSessionsManager( private val store: WalletConnectStore, - private val getWallets: GetWalletsUseCase, + getWallets: GetWalletsUseCase, + multiAccountListSupplier: MultiAccountListSupplier, private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, private val analytics: AnalyticsEventHandler, @@ -38,18 +45,31 @@ internal class DefaultWcSessionsManager( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) - override val sessions: Flow>> - get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } - .transform { pair -> - val (wallets, inStore) = pair - val inSdk: List = WalletKit.getListOfActiveSessions() - val associatedSessions: List = associate(inSdk, inStore, wallets) - val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) - if (someRemove) return@transform // ignore emit, wait next one - emit(associatedSessions.groupBy { it.wallet }) - } - .distinctUntilChanged() - .flowOn(dispatchers.io) + override val sessions: Flow>> = combine( + flow = getWallets(), + flow2 = store.sessions, + flow3 = multiAccountListSupplier.invokeMap(), + transform = ::Triple, + ) + .transformLatest { triple -> + val (wallets, inStore, allWalletsAccounts) = triple + val inSdk: List = WalletKit.getListOfActiveSessions() + val associatedSessions: List = associate( + inSdk = inSdk, + inStore = inStore, + wallets = wallets, + allWalletsAccounts = allWalletsAccounts, + ) + removeUnknownSessions(inStore, inSdk, associatedSessions) + emit(associatedSessions.groupBy { it.wallet }) + } + .distinctUntilChanged() + .flowOn(dispatchers.io) + .shareIn( + scope = scope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 0, replayExpirationMillis = 0), + replay = 1, + ) override fun onWcSdkInit() { listenOnSessionDelete() @@ -79,6 +99,7 @@ internal class DefaultWcSessionsManager( inSdk: List, inStore: Set, wallets: List, + allWalletsAccounts: LinkedHashMap, ): List { // if the WcSdk `onSessionSettleResponse` callback arrives late, merge pending approvals with WcSdk sessions val savedPending = store.pendingApproval.first() @@ -92,7 +113,9 @@ internal class DefaultWcSessionsManager( val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null - val account = wcNetworksConverter.getAccount(storeSession.accountId) as? Account.CryptoPortfolio + val walletAccounts = allWalletsAccounts[storeSession.walletId] ?: return@mapNotNull null + val account = walletAccounts.accounts + .find { account -> account.accountId == storeSession.accountId } as? Account.CryptoPortfolio ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession) val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: "" diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 9104712d62..8ef4d7a69e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -6,10 +6,8 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.model.CAIP10 -import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus @@ -28,7 +26,6 @@ internal class WcNetworksConverter @Inject constructor( private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val singleAccountSupplier: SingleAccountSupplier, ) { fun createNetwork(chainId: String, wallet: UserWallet): Network? { @@ -117,10 +114,6 @@ internal class WcNetworksConverter @Inject constructor( return existNetworks } - suspend fun getAccount(accountId: AccountId): Account? { - return singleAccountSupplier.getSyncOrNull(SingleAccountProducer.Params(accountId)) - } - suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { val portfolioNetworks = getAccountNetworks(sessionForApprove.account.accountId) return sessionForApprove.network diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt index 45989af478..a914d2b6f4 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt @@ -3,7 +3,9 @@ package com.tangem.domain.account.supplier import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Supplier that provides a list of [AccountList]s for all user wallets. @@ -18,4 +20,12 @@ abstract class MultiAccountListSupplier( operator fun invoke(): Flow> { return super.invoke(params = Unit) } + + fun invokeMap(): Flow> = invoke() + .map { accountLists -> + accountLists.associateByTo( + destination = linkedMapOf(), + keySelector = { accountList -> accountList.userWalletId }, + ) + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index b3de223ee0..f7eb452cc2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -191,4 +191,14 @@ sealed class TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa KYC Canceled", ) + + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Banner Clicked", + ) + + class DetailsVisaPermanentButtonClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Button Clicked", + ) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 8ebb80ce0b..1cf9bc5fc0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -264,6 +265,7 @@ internal class DetailsModel @Inject constructor( private fun onTangemPayItemClicked() { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.DetailsVisaPermanentButtonClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt index 0c3b768d31..921f2d9db3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt @@ -1,6 +1,5 @@ package com.tangem.features.hotwallet.common.repository -import android.content.Context import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.EntropyLength import com.tangem.crypto.bip39.Mnemonic @@ -8,15 +7,13 @@ import com.tangem.crypto.bip39.Wordlist import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.MnemonicRepository.MnemonicType import com.tangem.sdk.extensions.getWordlist -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -internal class DefaultMnemonicRepository @Inject constructor( - @ApplicationContext private val context: Context, -) : MnemonicRepository { - private val wordlist = Wordlist.getWordlist(context) +internal class DefaultMnemonicRepository @Inject constructor() : MnemonicRepository { - override val words: Set = wordlist.words.toHashSet() + private val wordlist by lazy(LazyThreadSafetyMode.NONE) { Wordlist.getWordlist() } + + override val words: Set by lazy(LazyThreadSafetyMode.NONE) { wordlist.words.toHashSet() } override fun generateMnemonic(type: MnemonicType): Mnemonic = DefaultMnemonic( entropy = when (type) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index d4c055fcda..990778ca91 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -59,7 +59,7 @@ internal fun MultiWalletAccessCodeEnter( val focusRequester = remember { FocusRequester() } OutlineTextField( - modifier = modifier + modifier = Modifier .focusRequester(focusRequester) .fillMaxWidth(), value = if (reEnterAccessCodeState) { @@ -73,12 +73,14 @@ internal fun MultiWalletAccessCodeEnter( state.onAccessCodeFirstChange }, label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), - isError = state.codesNotMatchError, + isError = state.codesNotMatchError || state.atLeast4CharError, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) + state.atLeast4CharError && !reEnterAccessCodeState -> + stringResourceSafe(R.string.onboarding_access_code_too_short) else -> null }, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index c9e17d71f5..4ada6b4b01 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -125,7 +125,6 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } - subscribeToWithdrawOrder() } fun onPause() { @@ -150,14 +149,6 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } - private fun subscribeToWithdrawOrder() { - modelScope.launch { - val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() - ?: return@launch - tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) - } - } - override fun onClickPinCode() { analytics.send(TangemPayAnalyticsEvents.PinCodeClicked()) if (!params.config.isPinSet) { 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 a8626464c7..ac1438b0d6 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 @@ -737,15 +737,20 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol)) modelScope.launch { - val canHide = cryptoCurrency is CryptoCurrency.Coin && isCryptoCurrencyCoinCouldHideUseCase( - userWalletId = userWalletId, - cryptoCurrencyCoin = cryptoCurrency, - ) + val canHide = when (cryptoCurrency) { + is CryptoCurrency.Coin -> { + isCryptoCurrencyCoinCouldHideUseCase( + userWalletId = userWalletId, + cryptoCurrencyCoin = cryptoCurrency, + ) + } + is CryptoCurrency.Token -> true + } - internalUiState.value = if (!canHide) { - stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) - } else { + internalUiState.value = if (canHide) { stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + } else { + stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt index d6fbf57989..2d1b76c423 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt @@ -5,7 +5,6 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem @@ -70,7 +69,7 @@ internal class AccountTokenItemConverter( tokenItemState = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = accountStatus.account, - ).convert(TotalFiatBalance.Loading), + ).convert(accountStatus.tokenList.totalFiatBalance), ), ) if (isGrouping) { 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 473bcd8df2..3e3e67cf00 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 @@ -245,6 +245,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( override fun onOnboardingBannerClick(userWalletId: UserWalletId) { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.MainVisaPermanentBannerClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 98811cfe6f..4d3d8a240f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -292,7 +293,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) + router.dialogNavigation.dismiss() }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 3f4ca9070d..5485587450 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -138,7 +138,7 @@ internal object WalletScreenPreviewDataLegacy { TokensListItemUM.Portfolio( content = PortfolioItemContentUM.Empty( action = PortfolioItemContentUM.Empty.Action( - text = stringReference("Manage tokens"), + text = resourceReference(id = R.string.onboarding_add_tokens), onClick = {}, ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt index 088110a3cb..cb7eb1f324 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt @@ -1,14 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -27,18 +28,23 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( return@map false } - return@map walletManagersFacade.validateSignatureCount( - userWalletId = userWallet.walletId, - network = network, - signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, - ) - .fold( - ifLeft = { true }, - ifRight = { - cardRepository.setCardWasScanned(cardId = userWallet.cardId) - false - }, + return@map try { + walletManagersFacade.validateSignatureCount( + userWalletId = userWallet.walletId, + network = network, + signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, ) + .fold( + ifLeft = { true }, + ifRight = { + cardRepository.setCardWasScanned(cardId = userWallet.cardId) + false + }, + ) + } catch (e: IllegalArgumentException) { + Timber.w(e, "Unable to validate signature count: user wallet not found") + false + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index bf2960f3d7..f30c88927b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -17,6 +17,11 @@ internal class DeleteWalletTransformer( val deletedWalletUM = prevState.getDeletedWalletState2() return when { + deletedWalletUM != null && deletedWalletState != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), + ) deletedWalletUM != null -> prevState.copy( selectedWalletIndex = selectedWalletIndex, wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index c33c75e00e..1ad54a1a82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -142,7 +142,7 @@ internal class TokenListStateConverter( is WalletTokensListState.Empty -> listOf() } val onEmptyAction = PortfolioItemContentUM.Empty.Action( - text = resourceReference(id = R.string.main_manage_tokens), + text = resourceReference(id = R.string.onboarding_add_tokens), onClick = { clickIntents.onManageTokensClick(account.accountId) }, ) return TokensListPortfolioItemConverter( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 1dcc11ff2a..04f33d8338 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -6,6 +6,7 @@ import com.tangem.domain.pay.model.MainCustomerInfoContentState import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber @Suppress("LongParameterList") @@ -28,10 +30,14 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( private val clickIntents: WalletClickIntents, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val analytics: WalletTangemPayAnalyticsEventSender, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> { + coroutineScope.launch { + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) + } return subscribeOnTangemPayInfoUpdates() } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 3e5fe1fd8a..034e3562ab 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-582" #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-539" +tangemHotSdk = "develop-547" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index a6765c196a..4c04ab93ee 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -1,5 +1,6 @@ package com.tangem.blockchainsdk.providers +import android.os.Build import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage @@ -28,7 +29,21 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( /** Load [BlockchainProvidersResponse] */ suspend fun load(): BlockchainProvidersResponse? { - val localResponse = loadLocal().ifEmpty { return null } + val localResponse = loadLocal() + + /** + * Behavior when local config is empty or cannot be parsed: + * - Android 15 -> return null (do not load remote) + * - Android 16 (stable) -> return null (do not load remote) + * - Android 16 (preview)-> continue (attempt to load remote and merge) + * - Android 17+ -> continue (attempt to load remote and merge) + */ + val shouldLoadRemoteIfError = Build.VERSION.SDK_INT > ANDROID_16_SDK_VERSION || + Build.VERSION.SDK_INT == ANDROID_16_SDK_VERSION && Build.VERSION.PREVIEW_SDK_INT > 0 + + if (localResponse.isEmpty() && !shouldLoadRemoteIfError) { + return null + } return loadRemote().fold( onSuccess = { remoteResponse -> @@ -39,7 +54,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( }, onFailure = { throwable -> Timber.e(throwable, "Failed to load blockchain provider types from backend") - localResponse + localResponse.ifEmpty { null } }, ) } @@ -50,4 +65,9 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( dispatcher = dispatchers.io, block = tangemTechApi::getBlockchainProviders, ) + + private companion object { + // TODO Replace with Build.VERSION_CODES + const val ANDROID_16_SDK_VERSION = 36 + } } \ No newline at end of file