diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a5d1a89425..2d18650f6d 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994 +Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6 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 4dc9cefd1a..5708a79d8b 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -2,6 +2,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.data.pay.util.WithdrawStateConverter +import com.tangem.data.pay.util.WithdrawStoreDataConverter import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -11,6 +14,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -41,6 +45,8 @@ internal class DefaultTangemPayStorage @Inject constructor( } private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) } + private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() } + private val withdrawStateConverter by lazy { WithdrawStateConverter() } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { @@ -131,10 +137,12 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) - .plus(createWithdrawOrderIdKey(userWalletId) to orderId) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) + .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, value = orders, @@ -142,14 +150,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } } - override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) - return orders[createWithdrawOrderIdKey(userWalletId)] + override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? { + val orders = appPreferencesStore.getObjectMapSync( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) + val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null + return withdrawStateConverter.convert(data) } override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 8c8404b2d3..c99860711a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module import dagger.Provides @@ -40,4 +43,28 @@ internal object HotWalletDomainModule { ): IsHotWalletCreationSupported { return IsHotWalletCreationSupported(hotWalletRepository) } + + @Provides + @Singleton + fun provideCheckHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CheckHotWalletUpgradeBannerUseCase { + return CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideCloseHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CloseHotWalletUpgradeBannerUseCase { + return CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideShouldShowUpgradeHotWalletBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): ShouldShowUpgradeHotWalletBannerUseCase { + return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) + } } \ No newline at end of file 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 3e6b36a6a5..39cae1ec11 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 @@ -123,7 +123,13 @@ internal class DefaultTangemSdkManager( } if (awaitInitialization) { - awaitAuthenticationManagerInitialization().needEnrollBiometrics + val manager = awaitAuthenticationManagerInitialization() + + if (manager.isInitialized) { + manager.needEnrollBiometrics + } else { + false + } } else { throw e } @@ -142,7 +148,11 @@ internal class DefaultTangemSdkManager( if (awaitInitialization) { val manager = awaitAuthenticationManagerInitialization() - manager.canAuthenticate || manager.needEnrollBiometrics + if (manager.isInitialized) { + manager.canAuthenticate || manager.needEnrollBiometrics + } else { + false + } } else { throw e } 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 1a804c5896..23de2f59f4 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 @@ -42,7 +42,7 @@ }, { "name": "SWAP_MARKET_LIST_ENABLED", - "version": "undefined" + "version": "5.34" }, { "name": "EARN_BLOCK_ENABLED", @@ -54,6 +54,6 @@ }, { "name": "WALLET_REORDER_FEATURE_ENABLED", - "version": "undefined" + "version": "5.34" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index a476b5dc1b..48cc8fc4fa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -27,6 +27,7 @@ data class OrderResponse( @Json(name = "emboss_name") val embossName: String?, @Json(name = "product_instance_id") val productInstanceId: String?, @Json(name = "payment_account_id") val paymentAccountId: String?, + @Json(name = "transaction_hash") val transactionHash: String?, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index eae141d861..e0c785a2e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -134,10 +134,6 @@ object PreferencesKeys { val SHOULD_SHOW_UPGRADE_BANNER_KEY by lazy { stringPreferencesKey(name = "shouldShowUpgradeBanner") } - val SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY by lazy { - stringPreferencesKey(name = "shouldShowNextTimeUpgradeBanner") - } - val UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "upgradeBannerClosureTimestamp") } val WALLET_CREATION_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "walletCreationTimestamp") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index 591bc9f6be..bd29637e98 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.visa import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens @Suppress("TooManyFunctions") @@ -28,9 +29,9 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? - suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) + suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? + suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 95ffc567c2..f460a6f129 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -103,9 +103,9 @@ object DateTimeFormatters { */ val localFullDate: DateTimeFormatter by lazy { val locale = Locale.getDefault() - val datePattern = DateFormat.getBestDateTimePattern(locale, "d MMMM") + val datePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, "d MMMM")) val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm" - val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton) + val timePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, timeSkeleton)) val fullPattern = "$datePattern, $timePattern" DateTimeFormatterBuilder() .appendPattern(fullPattern) @@ -128,13 +128,26 @@ object DateTimeFormatters { */ fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { val skeletonWithLocale = skeleton.replaceHourLetters() + val icuPattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale) + val jodaPattern = icuPatternToJodaPattern(icuPattern) return DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale)) + .appendPattern(jodaPattern) .toFormatter() .withLocale(Locale.getDefault()) } + /** + * Converts ICU date/time pattern (from [DateFormat.getBestDateTimePattern]) to Joda-Time compatible pattern. + */ + internal fun icuPatternToJodaPattern(icuPattern: String): String { + return icuPattern + .replace("LLLL", "MMMM") + .replace("LLL", "MMM") + .replace("LL", "MM") + .replace("L", "M") + } + private fun String.replaceHourLetters(): String { return if (is12HourFormat) { this.replace('H', 'h').replace('k', 'K') diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt new file mode 100644 index 0000000000..7fa8bcd9ce --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Unit tests for [DateTimeFormatters], in particular for conversion of ICU date/time patterns + * to Joda-Time compatible patterns. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateTimeFormattersTest { + + @Test + fun `converts LLLL to MMMM - full standalone month pattern that crashes on Chinese locale`() { + // Arrange + val icuPattern = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `converts LLL to MMM - short standalone month`() { + // Arrange + val icuPattern = "dd LLL yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd MMM yyyy") + } + + @Test + fun `converts LL to MM - numeric standalone month`() { + // Arrange + val icuPattern = "yyyy-MM-LL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("yyyy-MM-MM") + } + + @Test + fun `converts single L to M`() { + // Arrange + val icuPattern = "d/L/yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d/M/yyyy") + } + + @Test + fun `leaves pattern without L unchanged`() { + // Arrange + val icuPattern = "dd.MM.yyyy HH:mm" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd.MM.yyyy HH:mm") + } + + @Test + fun `leaves pattern with M unchanged`() { + // Arrange + val icuPattern = "d MMMM yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM yyyy") + } + + @Test + fun `handles mixed ICU pattern as returned for Chinese locale - d MMMM`() { + // Arrange + val icuPatternWithStandaloneMonth = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPatternWithStandaloneMonth) + + // Assert — Joda-Time can parse and format this without IllegalArgumentException + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `handles empty string`() { + // Arrange + val icuPattern = "" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `handles pattern with only literal characters`() { + // Arrange + val icuPattern = " 'at' " + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo(" 'at' ") + } + + @Test + fun `getBestFormatterBySkeleton with d MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("d MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } + + @Test + fun `getBestFormatterBySkeleton with dd MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 2fe0b92a3f..fbc60cc6c6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -47,8 +47,9 @@ internal class WalletAccountListFlowFactory @Inject constructor( return accountsResponseStoreFactory.create(userWallet.walletId).data .filterNotNull() + .filter { it.accounts.isNotEmpty() } .distinctUntilChanged() - .map(converter::convert) + .map { converter.convert(it) } } private fun createForSingleWallet(userWallet: UserWallet): AccountList { diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index bcda235987..335516f9fd 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -169,4 +169,42 @@ class WalletAccountListFlowFactoryTest { accountListConverter.convert(any()) } } + + @Test + fun `create for multi wallet with empty accounts does not emit`() = runTest { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + every { this@mockk.isMultiCurrency } returns true + } + + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + val accountsResponseWithEmptyAccounts = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = null, + sort = null, + totalAccounts = 0, + totalArchivedAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore + every { accountsResponseStore.data } returns accountsResponseStoreFlow + accountsResponseStoreFlow.value = accountsResponseWithEmptyAccounts + + // Act + val actual = factory.create(userWalletId).let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerify(inverse = true) { + accountListConverterFactory.create(any()) + accountListConverter.convert(any()) + } + } } \ No newline at end of file diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index 686f94579d..dbbe66db29 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -10,11 +10,14 @@ import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.concurrent.ConcurrentHashMap internal class DefaultHotWalletRepository( private val appPreferencesStore: AppPreferencesStore, ) : HotWalletRepository { + private val firstTopUpDetectedThisSession = ConcurrentHashMap() + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) override fun isWalletCreationSupported(): Boolean { return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q @@ -50,28 +53,12 @@ internal class DefaultHotWalletRepository( } } - override fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow = appPreferencesStore - .getObjectMap(PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY) - .map { it[userWalletId.stringValue] == true } - - override suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { - appPreferencesStore.editData { mutablePreferences -> - mutablePreferences.setObjectMap( - key = PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - value = mutablePreferences.getObjectMap( - PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - ) - .plus(userWalletId.stringValue to shouldShow), - ) - } - } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { return appPreferencesStore .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] } - override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) { + override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> mutablePreferences.setObjectMap( key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY, @@ -110,4 +97,12 @@ internal class DefaultHotWalletRepository( ) } } + + override fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean { + return firstTopUpDetectedThisSession.containsKey(userWalletId.stringValue) + } + + override fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) { + firstTopUpDetectedThisSession[userWalletId.stringValue] = Unit + } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 20d6cbf213..b229533df3 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -248,7 +248,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), - toExtraId = toExtraId, + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 49244f2da1..5c2bbe6654 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -1,12 +1,14 @@ package com.tangem.data.transaction import android.net.Uri -import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.NameResolver import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.blockchain.common.ReverseResolveAddressResult +import com.tangem.blockchain.common.TransactionValidator +import com.tangem.blockchain.common.memo.MemoState +import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -16,7 +18,6 @@ import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.errors.ParsedQrCodeErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext -import java.math.BigInteger class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, @@ -95,19 +96,28 @@ class DefaultWalletAddressServiceRepository( } } - override fun validateMemo(network: Network, memo: String): Boolean { - if (memo.isEmpty()) return true - return when (network.rawId) { - Blockchain.XRP.id -> { - val tag = memo.toLongOrNull() - tag != null && tag <= XRP_TAG_MAX_NUMBER + override suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean = + withContext(dispatchers.io) { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) ?: return@withContext true + + val memoStateResult = (walletManager as? TransactionValidator)?.validateMemo(memo) + if (memoStateResult != null) { + when (memoStateResult) { + is Result.Success -> when (memoStateResult.data) { + MemoState.NotSupported, + MemoState.Valid, + -> true + MemoState.Invalid -> false + } + is Result.Failure -> true + } + } else { + true } - Blockchain.Stellar.id -> { - isAssignableXlmValue(memo) - } - else -> true } - } override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode { val blockchain = network.toBlockchain() @@ -145,26 +155,4 @@ class DefaultWalletAddressServiceRepository( private fun Blockchain.isNear(): Boolean { return this == Blockchain.Near || this == Blockchain.NearTestnet } - - private fun isAssignableXlmValue(value: String): Boolean { - return when { - value.isNotEmpty() && value.isDigitsOnly() -> { - try { - // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo - value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() - } catch (ex: NumberFormatException) { - false - } - } - else -> { - // from org.stellar.sdk.MemoText - value.toByteArray().size <= XLM_MEMO_MAX_LENGTH - } - } - } - - companion object { - private const val XLM_MEMO_MAX_LENGTH = 28 - private const val XRP_TAG_MAX_NUMBER = 4294967295 - } } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 22542554f7..2c2683c14e 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.quotes) implementation(projects.domain.common) + implementation(projects.features.swap.domain) /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 500e5fa50d..5868e4c0c6 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -45,7 +45,7 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository + fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository @Binds @Singleton diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt new file mode 100644 index 0000000000..cdad87d12b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt @@ -0,0 +1,19 @@ +package com.tangem.data.pay.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +data class WithdrawStoreData( + @Json(name = "orderId") val orderId: String, + @Json(name = "exchangeData") val exchangeData: ExchangeStoreData?, +) + +@JsonClass(generateAdapter = false) +data class ExchangeStoreData( + @Json(name = "txId") val txId: String, + @Json(name = "fromNetwork") val fromNetwork: String, + @Json(name = "fromAddress") val fromAddress: String, + @Json(name = "payInAddress") val payInAddress: String, + @Json(name = "payInExtraId") val payInExtraId: String?, +) \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index 11afd970e8..f479794766 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.visa.error.VisaApiError @@ -12,35 +12,23 @@ import javax.inject.Inject internal class DefaultCustomerOrderRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, - private val tangemPayStorage: TangemPayStorage, ) : CustomerOrderRepository { - override suspend fun getOrderStatus( - userWalletId: UserWalletId, - orderId: String, - ): Either { + override suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either { return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> - when (response.result?.status) { + val status = when (response.result?.status) { null -> OrderStatus.UNKNOWN OrderStatus.NEW.apiName -> OrderStatus.NEW OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED else -> OrderStatus.CANCELED } + OrderData( + status = status, + withdrawTxHash = response.result?.data?.transactionHash?.ifEmpty { null }, + ) } } - - override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean { - val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId) - if (orderId == null) return false - - val status = getOrderStatus(userWalletId, orderId).getOrNull() - - val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING - if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId) - - return hasActiveOrder - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt deleted file mode 100644 index 6bc1774f28..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.data.pay.repository - -import arrow.core.Either -import arrow.core.left -import com.tangem.core.error.UniversalError -import com.tangem.data.common.quote.QuotesFetcher -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest -import com.tangem.datasource.api.pay.models.request.WithdrawRequest -import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.WithdrawalSignatureResult -import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.repository.TangemPaySwapRepository -import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.extensions.addHexPrefix -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.Currency -import java.util.Locale -import javax.inject.Inject - -@Suppress("LongParameterList") -internal class DefaultTangemPaySwapRepository @Inject constructor( - private val tangemPayApi: TangemPayApi, - private val requestHelper: TangemPayRequestPerformer, - private val authDataSource: TangemPayAuthDataSource, - private val quotesFetcher: QuotesFetcher, - private val tangemPayStorage: TangemPayStorage, -) : TangemPaySwapRepository { - - override suspend fun withdraw( - userWallet: UserWallet, - receiverAddress: String, - cryptoAmount: BigDecimal, - cryptoCurrencyId: CryptoCurrency.RawID, - ): Either { - val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) - if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) - return requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) - tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) - }.map { data -> - val result = data.result ?: return VisaApiError.WithdrawalDataError.left() - val signatureResult = authDataSource.getWithdrawalSignature( - userWallet = userWallet, - hash = result.hash, - ).getOrNull() - - return when (signatureResult) { - is WithdrawalSignatureResult.Cancelled -> { - Either.Right(WithdrawalResult.Cancelled) - } - is WithdrawalSignatureResult.Success -> { - requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawRequest( - amountInCents = amountInCents, - recipientAddress = receiverAddress, - adminSalt = result.salt, - senderAddress = result.senderAddress, - adminSignature = signatureResult.signature.addHexPrefix(), - ) - tangemPayApi.withdraw(authHeader = authHeader, body = request) - } - .mapLeft { return Either.Left(VisaApiError.WithdrawError) } - .map { response -> - val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId) - WithdrawalResult.Success - } - } - null -> return Either.Left(VisaApiError.SignWithdrawError) - } - } - } - - private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { - val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null - val amountInDollars = cryptoAmount.multiply(fiatRate) - val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits - return amountInDollars - .setScale(defaultFractionDigits, RoundingMode.HALF_UP) - .movePointRight(defaultFractionDigits) - .longValueExact() - .toString() - } - - private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { - val quotes = quotesFetcher.fetch( - fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, - currencyId = cryptoCurrencyId.value, - field = QuotesFetcher.Field.PRICE, - ).getOrNull() - - return quotes?.quotes[cryptoCurrencyId.value]?.price - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..cea8e61d60 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -0,0 +1,301 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.core.error.UniversalError +import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.datasource.api.pay.TangemPayApi +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.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.model.OrderData +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +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.extensions.addHexPrefix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +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 +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.Currency +import java.util.Locale +import javax.inject.Inject +import kotlin.collections.set +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds + +private const val TAG = "TangemPaySwapRepository" + +@Suppress("LongParameterList") +internal class DefaultTangemPayWithdrawRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val authDataSource: TangemPayAuthDataSource, + private val quotesFetcher: QuotesFetcher, + private val tangemPayStorage: TangemPayStorage, + private val swapRepository: SwapRepository, + private val orderRepository: CustomerOrderRepository, +) : TangemPayWithdrawRepository { + + private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val withdrawPollingJobs = mutableMapOf() + private val withdrawPollingMutex = Mutex() + + override suspend fun withdraw( + userWallet: UserWallet, + receiverAddress: String, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, + ): Either { + val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) + if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) + return requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) + tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) + }.map { data -> + val result = data.result ?: return VisaApiError.WithdrawalDataError.left() + val signatureResult = authDataSource.getWithdrawalSignature( + userWallet = userWallet, + hash = result.hash, + ).getOrNull() + + return when (signatureResult) { + is WithdrawalSignatureResult.Cancelled -> { + Either.Right(WithdrawalResult.Cancelled) + } + is WithdrawalSignatureResult.Success -> { + requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawRequest( + amountInCents = amountInCents, + recipientAddress = receiverAddress, + adminSalt = result.salt, + senderAddress = result.senderAddress, + adminSignature = signatureResult.signature.addHexPrefix(), + ) + tangemPayApi.withdraw(authHeader = authHeader, body = request) + } + .mapLeft { return Either.Left(VisaApiError.WithdrawError) } + .map { response -> + processWithdrawResult(response, userWallet, exchangeData) + WithdrawalResult.Success + } + } + null -> return Either.Left(VisaApiError.SignWithdrawError) + } + } + } + + private suspend fun processWithdrawResult( + response: WithdrawResponse, + userWallet: UserWallet, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val orderId = response.result?.orderId + if (orderId != null) { + val orderData = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = orderData?.withdrawTxHash + val storeData = TangemPayWithdrawState( + orderId = orderId, + exchangeData = exchangeData, + ) + if (orderData != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = withdrawTxHash, + orderId = orderId, + exchangeData = exchangeData, + order = orderData, + ).onLeft { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } + } else { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } + } + } + + private suspend fun finalizeWithdraw( + userWallet: UserWallet, + withdrawTxHash: String, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + order: OrderData, + ): Either { + return swapRepository.exchangeSent( + userWallet = userWallet, + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + txHash = withdrawTxHash, + payInExtraId = exchangeData.payInExtraId, + ) + .onRight { + val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId) + } else { + tangemPayStorage.storeWithdrawOrder( + userWalletId = userWallet.walletId, + data = TangemPayWithdrawState(orderId = orderId, exchangeData = null), + ) + } + } + .onLeft { error -> + Timber.tag(TAG).e(error.toString()) + } + } + + override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { + val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) + if (orderExchangeData == null) return false + + val exchangeData = orderExchangeData.exchangeData + val orderData = orderRepository + .getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull() + val withdrawTxHash = orderData?.withdrawTxHash + + if (exchangeData != null && orderData != null && withdrawTxHash != null) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = withdrawTxHash, + orderId = orderExchangeData.orderId, + exchangeData = exchangeData, + order = orderData, + ) + } + + return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + } + + override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either { + val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right() + val exchangeData = storeData.exchangeData ?: return Unit.right() + + val orderId = storeData.orderId + val order = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return Unit.right() + + val txHash = order.withdrawTxHash + + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = txHash, + orderId = storeData.orderId, + exchangeData = exchangeData, + order = order, + ).onLeft { + startWithdrawOrderPolling( + userWallet = userWallet, + orderId = orderId, + storeData = storeData, + exchangeData = exchangeData, + ) + } + } else { + startWithdrawOrderPolling( + userWallet = userWallet, + orderId = orderId, + storeData = storeData, + exchangeData = exchangeData, + ) + } + + return Unit.right() + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + storeData: TangemPayWithdrawState, + exchangeData: TangemPayWithdrawExchangeState, + ) { + withdrawPollingMutex.withLock { + if (withdrawPollingJobs.containsKey(orderId)) return + + val pollingJob = withdrawPollingScope.launch { + try { + while (isActive && withdrawPollingJobs.containsKey(orderId)) { + delay(duration = 5.seconds) + + val orderData = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + orderData.onRight { order -> + if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) { + tangemPayStorage.deleteWithdrawOrder(userWallet.walletId) + withdrawPollingJobs.remove(key = orderId) + return@launch + } + val txHash = order.withdrawTxHash + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = txHash, + orderId = storeData.orderId, + exchangeData = exchangeData, + order = order, + ).onRight { + withdrawPollingJobs.remove(key = orderId) + return@launch + } + } + }.onLeft { error -> + Timber.tag(TAG).e("error ${error.errorCode}") + } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + } + } + withdrawPollingJobs[orderId] = pollingJob + } + } + + private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { + val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null + val amountInDollars = cryptoAmount.multiply(fiatRate) + val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits + return amountInDollars + .setScale(defaultFractionDigits, RoundingMode.HALF_UP) + .movePointRight(defaultFractionDigits) + .longValueExact() + .toString() + } + + private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { + val quotes = quotesFetcher.fetch( + fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, + currencyId = cryptoCurrencyId.value, + field = QuotesFetcher.Field.PRICE, + ).getOrNull() + + return quotes?.quotes[cryptoCurrencyId.value]?.price + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index 36b85985ad..73f569a219 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -4,14 +4,15 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.repository.TangemPaySwapRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import java.math.BigDecimal import javax.inject.Inject internal class DefaultTangemPayWithdrawUseCase @Inject constructor( - private val repository: TangemPaySwapRepository, + private val repository: TangemPayWithdrawRepository, ) : TangemPayWithdrawUseCase { override suspend fun invoke( @@ -19,12 +20,14 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either { return repository.withdraw( userWallet = userWallet, cryptoAmount = cryptoAmount, receiverAddress = receiverCexAddress, cryptoCurrencyId = cryptoCurrencyId, + exchangeData = exchangeData, ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt new file mode 100644 index 0000000000..5bcefd3b8e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStateConverter : Converter { + + override fun convert(value: WithdrawStoreData): TangemPayWithdrawState = TangemPayWithdrawState( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + TangemPayWithdrawExchangeState( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt new file mode 100644 index 0000000000..e46044b735 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.ExchangeStoreData +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStoreDataConverter : Converter { + + override fun convert(value: TangemPayWithdrawState): WithdrawStoreData = WithdrawStoreData( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + ExchangeStoreData( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + ) +} \ No newline at end of file diff --git a/domain/hot-wallet/build.gradle.kts b/domain/hot-wallet/build.gradle.kts index 471afb3452..621b607241 100644 --- a/domain/hot-wallet/build.gradle.kts +++ b/domain/hot-wallet/build.gradle.kts @@ -14,4 +14,10 @@ dependencies { implementation(projects.domain.wallets.models) implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..1038773b7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke( + walletId: UserWalletId, + hasBalance: Boolean, + shouldShowUpgradeBanner: Boolean, + ): Either = try { + val currentTime = System.currentTimeMillis() + val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) + + val creationTimestampActual = if (creationTimestamp == null) { + // If creationTimestamp is null (wallet was created before this feature was released), + // store the current timestamp and use it below + hotWalletRepository.setWalletCreationTimestamp(walletId, currentTime) + currentTime + } else { + creationTimestamp + } + + val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) + + val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) + val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } + + // Wallet balance is positive, but the first top-up hasn't been tracked yet + if (hasBalance && !hasHadFirstTopUp) { + hotWalletRepository.setHasHadFirstTopUp(walletId, true) + hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) + hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) + } + + val shouldShow = when { + // Banner should be shown (e.g., because of the first top-up in the previous session) + shouldShowUpgradeBanner -> !hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) + // Banner was closed; it happened more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp != null && daysSinceClosure != null && daysSinceClosure >= BANNER_RESHOW_DAYS -> true + // Banner hasn't been closed; wallet was created more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp == null && daysSinceCreation >= BANNER_RESHOW_DAYS -> true + else -> false + } + shouldShow.right() + } catch (e: Exception) { + e.left() + } + + companion object { + const val BANNER_RESHOW_DAYS = 30L + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..17591a2bb1 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId + +class CloseHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke(walletId: UserWalletId): Either = try { + val currentTime = System.currentTimeMillis() + hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime) + Unit.right() + } catch (e: Exception) { + e.left() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt new file mode 100644 index 0000000000..49e17e6c7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class ShouldShowUpgradeHotWalletBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow = + hotWalletRepository.shouldShowUpgradeBanner(userWalletId) +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 2432bc3dc9..01229d8d3a 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,13 +17,9 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow - - suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? - suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) + suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? @@ -32,4 +28,8 @@ interface HotWalletRepository { suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) + + fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean + + fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..6e415efea4 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,257 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setWalletCreationTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists and 30 days since closure WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists but less than 30 days since closure WHEN invoke THEN return false`() = + runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set but closure timestamp exists WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setHasHadFirstTopUp(walletId, true) } + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) } + verify { hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) } + } + + @Test + fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } + coVerify(exactly = 0) { hotWalletRepository.setShouldShowUpgradeBanner(any(), any()) } + } + + @Test + fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result1 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result2 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result3 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat((result1 as Either.Right).value).isTrue() + assertThat((result2 as Either.Right).value).isTrue() + assertThat((result3 as Either.Right).value).isTrue() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..d961d07617 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CloseHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `WHEN invoke THEN set banner flag to false and closure timestamp`() = runTest { + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Right::class.java) + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Either Left`() = runTest { + val exception = RuntimeException("Test error") + coEvery { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } throws exception + + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Left::class.java) + assertThat((result as Either.Left).value).isEqualTo(exception) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index f566a5a93b..e43d756df6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -23,7 +23,7 @@ interface WalletAddressServiceRepository { suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean - fun validateMemo(network: Network, memo: String): Boolean + suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index 364eef4916..eb69e7feff 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -3,7 +3,8 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.models.network.Network +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.ValidateMemoError @@ -14,9 +15,17 @@ class ValidateWalletMemoUseCase( private val walletAddressServiceRepository: WalletAddressServiceRepository, ) { - operator fun invoke(network: Network, memo: String): Either { + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + memo: String, + ): Either { return try { - val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo) + val isValidMemo = walletAddressServiceRepository.validateMemo( + userWalletId = userWalletId, + network = cryptoCurrency.network, + memo = memo, + ) if (isValidMemo) { Unit.right() } else { diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index cab4b8254c..bc0e47c44b 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.features.swap.domain) /** Security */ diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt new file mode 100644 index 0000000000..a1424c3acd --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.pay + +data class TangemPayWithdrawState( + val orderId: String, + val exchangeData: TangemPayWithdrawExchangeState?, +) + +data class TangemPayWithdrawExchangeState( + val txId: String, + val fromNetwork: String, + val fromAddress: String, + val payInAddress: String, + val payInExtraId: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt new file mode 100644 index 0000000000..42b7001564 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pay.model + +data class OrderData( + val status: OrderStatus, + val withdrawTxHash: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt index 9ae1fd414b..09b25edc3a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt @@ -2,12 +2,10 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.visa.error.VisaApiError interface CustomerOrderRepository { - suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either - - suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean + suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt similarity index 58% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index fed66aab7d..18b09d331e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -4,15 +4,22 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.visa.error.VisaApiError import java.math.BigDecimal -interface TangemPaySwapRepository { +interface TangemPayWithdrawRepository { suspend fun withdraw( userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, ): Either + + suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean + + suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 7ff408d1c1..647e6ad412 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -137,13 +137,13 @@ class TangemPayMainScreenCustomerInfoUseCase( userWalletId: UserWalletId, orderId: String, ): Either { - return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId) + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId) .fold( ifLeft = { error -> error.mapErrorForCustomer().left() }, - ifRight = { orderStatus -> - when (orderStatus) { + ifRight = { orderData -> + when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, @@ -154,7 +154,7 @@ class TangemPayMainScreenCustomerInfoUseCase( kycStatus = CustomerInfo.KycStatus.APPROVED, cardInfo = null, ), - orderStatus = orderStatus, + orderStatus = orderData.status, ).right() // Order was created/cancelled -> clear order id and get customer info @@ -164,11 +164,11 @@ class TangemPayMainScreenCustomerInfoUseCase( -> { onboardingRepository.clearOrderId(userWalletId) // If order was cancelled -> start order creation - if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) + if (orderData.status == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) onboardingRepository.getCustomerInfo(userWalletId = userWalletId) .mapLeft { it.mapErrorForCustomer() } .map { customerInfo -> - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) } } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index e2abfd227f..d98264599f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal @@ -14,5 +15,6 @@ interface TangemPayWithdrawUseCase { cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index be4e913124..850782d36a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -171,6 +171,10 @@ sealed class WalletSettingsAnalyticEvents( event = "Wallet Upgraded", ), AppsFlyerIncludedEvent + class WalletsReorder : WalletSettingsAnalyticEvents( + event = "Longtap - Wallets Order", + ) + enum class RecoveryPhraseScreenAction(val value: String) { Upgrade("Upgrade"), Backup("Backup"), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 5d48231c07..792db8d675 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.fields.AutoSizeTextField import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account @@ -53,11 +55,12 @@ internal fun AccountCreateEditContent( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() + Column( modifier = modifier .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() - .imePadding() .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -65,6 +68,7 @@ internal fun AccountCreateEditContent( Column( modifier = Modifier + .nestedScroll(nestedScrollConnection) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) .weight(1f), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 5c011e92c8..44af704d49 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM @@ -138,10 +139,11 @@ internal class UserWalletListModel @Inject constructor( val userWalletIds = state.value.userWallets.map { UserWalletId(it.id) } modelScope.launch { - applyUserWalletListSortingUseCase(userWalletIds) - .onLeft { error -> - Timber.e("Failed to apply wallet list sorting: $error") - } + applyUserWalletListSortingUseCase(userWalletIds).onRight { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder()) + }.onLeft { error -> + Timber.e("Failed to apply wallet list sorting: $error") + } } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index 8ba89fb021..feb3025f69 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt index b1052994f0..ef92d71b3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt @@ -10,10 +10,13 @@ import com.tangem.utils.StringsSigns import org.joda.time.DateTime internal fun mapFormattedDate(createdAt: String): TextReference { - val formattedDate = getFormattedDate( - createdAt = createdAt, - now = DateTime.now(), - ) + val formattedDate = runCatching { + getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + }.getOrElse { FormattedDate.FullDate("") } + return when (formattedDate) { is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) is FormattedDate.HoursAgo -> TextReference.PluralRes( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index a61f7cd842..f6d5e737b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index d92870b44f..e878df7dbd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -213,7 +213,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 95e67b5de3..96690cb647 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -323,8 +323,9 @@ internal class SendDestinationModel @Inject constructor( senderAddresses = senderAddresses.value, ) val memoValidationResult = validateWalletMemoUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, memo = memo.orEmpty(), - network = cryptoCurrency.network, ) if (type != null) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index 8de4dc6649..3634569cf9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -7,6 +7,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications import kotlinx.collections.immutable.ImmutableList @@ -40,6 +42,10 @@ internal class SwapNotificationsComponent( data class SwapNotificationData( val expressError: ExpressError?, val fromCryptoCurrency: CryptoCurrency?, + val destinationAddress: String, + val memo: String? = null, + val toCryptoCurrencyStatus: CryptoCurrencyStatus? = null, + val userWalletId: UserWalletId? = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index e2be8113cd..89e743866c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.swap.v2.impl.notifications.model +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -7,6 +8,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent @@ -14,6 +17,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import java.math.BigDecimal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -30,6 +34,7 @@ internal class SwapNotificationsModel @Inject constructor( private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, + private val validateTransactionUseCase: ValidateTransactionUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -61,12 +66,33 @@ internal class SwapNotificationsModel @Inject constructor( private suspend fun buildNotifications() { val notifications = buildList { addExpressErrorNotification() + addDestinationTagRequiredNotification() } swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty()) uiState.value = notifications.toImmutableList() } + private suspend fun MutableList.addDestinationTagRequiredNotification() { + val toCryptoCurrencyStatus = notificationData.toCryptoCurrencyStatus ?: return + val userWalletId = notificationData.userWalletId ?: return + val destinationAddress = notificationData.destinationAddress + if (destinationAddress.isEmpty()) return + + val validationError = validateTransactionUseCase( + amount = BigDecimal.ZERO.convertToSdkAmount(toCryptoCurrencyStatus), + fee = null, + memo = notificationData.memo, + destination = destinationAddress, + userWalletId = userWalletId, + network = toCryptoCurrencyStatus.currency.network, + ).leftOrNull() + + if (validationError is BlockchainSdkError.DestinationTagRequired) { + add(NotificationUM.Error.DestinationTagRequired) + } + } + fun MutableList.addExpressErrorNotification() { val expressError = notificationData.expressError ?: return val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index aac3bb92dd..59d34c0701 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -133,6 +133,10 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( swapNotificationData = SwapNotificationsComponent.Params.SwapNotificationData( expressError = (model.confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = model.confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 6fdf6bdb46..271decfaa7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -429,6 +429,10 @@ internal class SendWithSwapConfirmModel @Inject constructor( data = SwapNotificationData( expressError = (confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ) uiState.transformerUpdate( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index d207e7992b..ef5dc2f371 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -312,7 +312,7 @@ internal class DefaultSwapRepository( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), - toExtraId = toExtraId, + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b74d66f7e2..02e0992d2b 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account) implementation(projects.domain.account.status) + implementation(projects.domain.visa.models) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) 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 060390b7a6..cbaa2d6509 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 @@ -36,6 +36,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.* @@ -1030,6 +1031,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError if (isTangemPayWithdrawal) { + val networkAddress = currencyToSend.value.networkAddress return SwapTransactionState.TangemPayWithdrawalData( cryptoAmount = amount.value, cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId), @@ -1056,6 +1058,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( txExternalId = exchangeDataCex.externalTxId, averageDuration = null, ), + exchangeData = TangemPayWithdrawExchangeState( + txId = exchangeDataCex.txId, + fromNetwork = currencyToSend.currency.network.backendId, + fromAddress = networkAddress?.defaultAddress?.value.orEmpty(), + payInAddress = exchangeData.transaction.txTo, + payInExtraId = exchangeDataCex.txExtraId, + ), ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 4619adf9ab..b1380f6a3a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -31,6 +32,7 @@ sealed class SwapTransactionState { val toAmount: String?, val toAmountValue: BigDecimal?, val storeData: StoreTransactionData, + val exchangeData: TangemPayWithdrawExchangeState, ) : SwapTransactionState() { data class StoreTransactionData( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index a90bd3640a..fb4e74e1ff 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1136,6 +1136,7 @@ internal class SwapModel @Inject constructor( cryptoAmount = swapTransactionState.cryptoAmount, cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, receiverCexAddress = swapTransactionState.cexAddress, + exchangeData = swapTransactionState.exchangeData, ) .onLeft { startLoadingQuotesFromLastState() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index a6ddf89de1..2a21dd13ec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -322,7 +322,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return 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 aa37f99d4a..793549861d 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 @@ -26,8 +26,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayTopUpData -import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayTxHistoryItem @@ -79,7 +79,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val cardDetailsEventListener: CardDetailsEventListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, - private val orderRepository: CustomerOrderRepository, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, @@ -124,6 +124,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } + subscribeToWithdrawOrder() } fun onPause() { @@ -148,6 +149,14 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeToWithdrawOrder() { + modelScope.launch { + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + ?: return@launch + tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet) + } + } + override fun onClickPinCode() { analytics.send(TangemPayAnalyticsEvents.PinCodeClicked()) if (!params.config.isPinSet) { @@ -278,24 +287,28 @@ internal class TangemPayDetailsModel @Inject constructor( if (currentBalance == null || depositAddress == null) { showBottomSheetError(TangemPayDetailsErrorType.Withdraw) } else { - modelScope.launch { - val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId) - if (hasActiveWithdrawal) { - showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) - } else { - val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() - val currency = cryptoCurrency ?: userWallet?.let { - tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId) - .getOrNull() - } - if (currency != null) { - uiMessageSender.send( - message = TangemPayMessagesFactory.createWithdrawWarning( - onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, - ), - ) + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + if (userWallet == null) { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } else { + modelScope.launch { + val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWallet = userWallet) + if (hasActiveWithdrawal) { + showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) } else { - showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + val currency = cryptoCurrency ?: tangemPayCryptoCurrencyFactory.create( + userWallet = userWallet, + chainId = params.config.chainId, + ).getOrNull() + if (currency != null) { + uiMessageSender.send( + message = TangemPayMessagesFactory.createWithdrawWarning( + onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, + ), + ) + } else { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ca5a2d4caa..0cdab04aa3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -17,7 +17,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -135,7 +135,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, - private val hotWalletRepository: HotWalletRepository, + private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -554,8 +554,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWalletId, false) appRouter.push(UpgradeWallet(userWalletId)) } } @@ -565,15 +563,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWalletId) - val currentTime = System.currentTimeMillis() - - if (hasHadFirstTopUp) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setUpgradeBannerClosureTimestamp(userWalletId, currentTime) - } else { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, true) - } + closeHotWalletUpgradeBannerUseCase(userWalletId) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt index 59095ad0df..b1c48cff7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt @@ -10,7 +10,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -43,10 +45,13 @@ internal class ExpandedAccountsHolder @Inject constructor( .toSet() // main state holder val expandedAccounts = MutableStateFlow(initExpandedState) + var debounceJob: Job? = null actionChannel .filter { (accountId, _) -> accountId.userWalletId == walletId } + .filter { debounceJob?.isActive != true } .onEach { (accountId, isExpand) -> + debounceJob = launch { delay(DEBOUNCE_MILLIS) } val newState = AccountExpandedState(accountId, isExpand) launch { accountsExpandedRepository.update(newState) } if (isExpand) { @@ -100,4 +105,8 @@ internal class ExpandedAccountsHolder @Inject constructor( } private fun walletAccounts(walletId: UserWalletId): Flow = singleAccountListSupplier(walletId) + + companion object { + private const val DEBOUNCE_MILLIS = 200L + } } \ No newline at end of file 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 6f8e6b1be6..6218c6257a 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 @@ -13,8 +13,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency @@ -44,7 +45,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import java.util.concurrent.TimeUnit + import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -60,7 +61,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, - private val hotWalletRepository: HotWalletRepository, + private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") @@ -101,8 +103,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) .distinctUntilChanged(), - hotWalletRepository.shouldShowUpgradeBanner(userWallet.walletId).distinctUntilChanged(), - hotWalletRepository.shouldShowNextTimeUpgradeBanner(userWallet.walletId).distinctUntilChanged(), + shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> @@ -117,7 +119,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean - val shouldShowNextTimeUpgradeBanner = array[9] as Boolean buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -129,7 +130,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, - shouldShowNextTimeUpgradeBanner = shouldShowNextTimeUpgradeBanner, ) addFinishWalletActivationNotification( @@ -464,45 +464,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - @Suppress("CyclomaticComplexMethod") private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, - shouldShowNextTimeUpgradeBanner: Boolean, ) { if (userWallet !is UserWallet.Hot) return - val currentTime = System.currentTimeMillis() - val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(userWallet.walletId) - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(userWallet.walletId) - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWallet.walletId) - - if (creationTimestamp == null) { - hotWalletRepository.setWalletCreationTimestamp(userWallet.walletId, currentTime) - return - } - - val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestamp) - val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } - if (hasBalance && !hasHadFirstTopUp) { - hotWalletRepository.setHasHadFirstTopUp(userWallet.walletId, true) - } - - val shouldShow = when { - shouldShowUpgradeBanner && hasBalance -> true - shouldShowNextTimeUpgradeBanner && daysSinceClosure != null && daysSinceClosure >= UPGRADE_BANNER_RESHOW_DAYS -> true - !shouldShowUpgradeBanner && !shouldShowNextTimeUpgradeBanner && hasHadFirstTopUp && daysSinceCreation >= UPGRADE_BANNER_RESHOW_DAYS -> { - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWallet.walletId, true) - true - } - else -> false - } + val shouldShow = checkHotWalletUpgradeBannerUseCase( + walletId = userWallet.walletId, + hasBalance = hasBalance, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + ).getOrNull() ?: return addIf( element = WalletNotification.UpgradeHotWalletPromo( @@ -515,6 +492,5 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 - const val UPGRADE_BANNER_RESHOW_DAYS = 30 } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 27856a0954..ae764cf797 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -251,7 +251,7 @@ internal enum class Wallet2CobrandImage( Sakura( cards2ResId = R.drawable.ill_sakura_card2_120_106, cards3ResId = R.drawable.ill_sakura_card3_120_106, - batchIds = setOf("AF990029", "AF990030", "AF990031"), + batchIds = setOf("AF990029", "AF990030", "AF990031", "AF990071", "AF990072", "AF990073"), ), SatoshiFriends( diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0f1801a9b1..ae62f1e18f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1425" +tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-578" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^