Updated on 2026-08-14
This commit is contained in:
commit
f5eaca92f6
64 changed files with 1271 additions and 301 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994
|
||||
Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6
|
||||
|
|
@ -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<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
|
||||
val orders = mutablePreferences.getObjectMap<TangemPayWithdrawState>(
|
||||
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<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? {
|
||||
val orders = appPreferencesStore.getObjectMapSync<WithdrawStoreData>(
|
||||
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<String>(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)
|
||||
val orders = mutablePreferences.getObjectMap<WithdrawStoreData>(
|
||||
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.minus(createWithdrawOrderIdKey(userWalletId))
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<UserWallet> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Unit>()
|
||||
|
||||
@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<Boolean> = appPreferencesStore
|
||||
.getObjectMap<Boolean>(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<Boolean>(
|
||||
PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY,
|
||||
)
|
||||
.plus(userWalletId.stringValue to shouldShow),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? {
|
||||
return appPreferencesStore
|
||||
.getObjectMapSync<Long>(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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ internal interface TangemPayDataModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository
|
||||
fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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<VisaApiError, OrderStatus> {
|
||||
override suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderData> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UniversalError, WithdrawalResult> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Job>()
|
||||
private val withdrawPollingMutex = Mutex()
|
||||
|
||||
override suspend fun withdraw(
|
||||
userWallet: UserWallet,
|
||||
receiverAddress: String,
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
exchangeData: TangemPayWithdrawExchangeState,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
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<ExpressDataError, Unit> {
|
||||
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<VisaApiError, Unit> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UniversalError, WithdrawalResult> {
|
||||
return repository.withdraw(
|
||||
userWallet = userWallet,
|
||||
cryptoAmount = cryptoAmount,
|
||||
receiverAddress = receiverCexAddress,
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
exchangeData = exchangeData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WithdrawStoreData, TangemPayWithdrawState> {
|
||||
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TangemPayWithdrawState, WithdrawStoreData> {
|
||||
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<Throwable, Boolean> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, Unit> = try {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
hotWalletRepository.setShouldShowUpgradeBanner(walletId, false)
|
||||
hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime)
|
||||
Unit.right()
|
||||
} catch (e: Exception) {
|
||||
e.left()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> =
|
||||
hotWalletRepository.shouldShowUpgradeBanner(userWalletId)
|
||||
}
|
||||
|
|
@ -17,13 +17,9 @@ interface HotWalletRepository {
|
|||
|
||||
suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean)
|
||||
|
||||
fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean>
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<ValidateMemoError, Unit> {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
memo: String,
|
||||
): Either<ValidateMemoError, Unit> {
|
||||
return try {
|
||||
val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo)
|
||||
val isValidMemo = walletAddressServiceRepository.validateMemo(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
memo = memo,
|
||||
)
|
||||
if (isValidMemo) {
|
||||
Unit.right()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
data class OrderData(
|
||||
val status: OrderStatus,
|
||||
val withdrawTxHash: String?,
|
||||
)
|
||||
|
|
@ -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<VisaApiError, OrderStatus>
|
||||
|
||||
suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean
|
||||
suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderData>
|
||||
}
|
||||
|
|
@ -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<UniversalError, WithdrawalResult>
|
||||
|
||||
suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean
|
||||
|
||||
suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit>
|
||||
}
|
||||
|
|
@ -137,13 +137,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
orderId: String,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UniversalError, WithdrawalResult>
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NotificationUM>.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<NotificationUM>.addExpressErrorNotification() {
|
||||
val expressError = notificationData.expressError ?: return
|
||||
val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1136,6 +1136,7 @@ internal class SwapModel @Inject constructor(
|
|||
cryptoAmount = swapTransactionState.cryptoAmount,
|
||||
cryptoCurrencyId = swapTransactionState.cryptoCurrencyId,
|
||||
receiverCexAddress = swapTransactionState.cexAddress,
|
||||
exchangeData = swapTransactionState.exchangeData,
|
||||
)
|
||||
.onLeft {
|
||||
startLoadingQuotesFromLastState()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AccountList> = singleAccountListSupplier(walletId)
|
||||
|
||||
companion object {
|
||||
private const val DEBOUNCE_MILLIS = 200L
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletNotification>.addUpgradeHotWalletPromoNotification(
|
||||
userWallet: UserWallet,
|
||||
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue