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