Updated on 2026-08-14
This commit is contained in:
commit
defd947695
44 changed files with 654 additions and 227 deletions
|
|
@ -109,8 +109,16 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
|||
isHidingEnabled = action.hideBalance,
|
||||
),
|
||||
)
|
||||
// state should be copied to avoid concurrent modifications from different sources
|
||||
is DetailsAction.AppSettings.Prepare -> state.copy(
|
||||
appSettingsState = action.state,
|
||||
appSettingsState = state.appSettingsState.copy(
|
||||
saveWallets = action.state.saveWallets,
|
||||
saveAccessCodes = action.state.saveAccessCodes,
|
||||
isBiometricsAvailable = action.state.isBiometricsAvailable,
|
||||
isHidingEnabled = action.state.isHidingEnabled,
|
||||
selectedAppCurrency = action.state.selectedAppCurrency,
|
||||
selectedThemeMode = action.state.selectedThemeMode,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics,
|
||||
is DetailsAction.AppSettings.CheckBiometricsStatus,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.common.extensions.toHexString
|
||||
|
|
@ -227,7 +228,7 @@ private fun navigateToWalletScreen() {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) {
|
||||
private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit, onFailure: (TangemError) -> Unit) {
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
||||
|
|
@ -248,6 +249,7 @@ private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) {
|
|||
Timber.e(it, "Unable to scan card")
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
onFailure(it)
|
||||
},
|
||||
onSuccess = onSuccess,
|
||||
)
|
||||
|
|
@ -606,9 +608,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
if (scanResponse == null) {
|
||||
scope.launch {
|
||||
readCard { newScanResponse ->
|
||||
handleFinishBackup(newScanResponse)
|
||||
}
|
||||
readCard(
|
||||
onSuccess = { newScanResponse ->
|
||||
handleFinishBackup(newScanResponse)
|
||||
},
|
||||
onFailure = {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleFinishBackup(scanResponse)
|
||||
|
|
@ -644,10 +651,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
} else {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
|
||||
readCard { newScanResponse ->
|
||||
scanResponse = newScanResponse
|
||||
userWallet = createUserWallet(newScanResponse, backupState)
|
||||
}
|
||||
readCard(
|
||||
onSuccess = { newScanResponse ->
|
||||
scanResponse = newScanResponse
|
||||
userWallet = createUserWallet(newScanResponse, backupState)
|
||||
},
|
||||
onFailure = {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ package com.tangem.tap.network.auth
|
|||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
||||
internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider {
|
||||
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
|
||||
|
||||
override fun getCardPublicKey(): String {
|
||||
return appStateHolder.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
|
||||
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
|
||||
}
|
||||
|
||||
override fun getCardId(): String {
|
||||
return appStateHolder.scanResponse?.card?.cardId ?: ""
|
||||
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: ""
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ import com.tangem.datasource.api.common.AuthProvider
|
|||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultAppVersionProvider
|
||||
import com.tangem.tap.network.auth.DefaultAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,8 +24,8 @@ class AuthModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider {
|
||||
return DefaultAuthProvider(appStateHolder)
|
||||
fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider {
|
||||
return DefaultAuthProvider(userWalletsListManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ sealed class GiveTxPermissionState {
|
|||
val cancelButton: CancelPermissionButton,
|
||||
val onChangeApproveType: ((ApproveType) -> Unit)? = null,
|
||||
) : GiveTxPermissionState()
|
||||
|
||||
fun GiveTxPermissionState.getApproveTypeOrNull(): ApproveType? {
|
||||
return (this as? ReadyForRequest)?.approveType
|
||||
}
|
||||
}
|
||||
|
||||
enum class ApproveType(val text: TextReference) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/** TangemTech [ApiConfig] */
|
||||
internal class TangemTech(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
|
@ -29,5 +31,6 @@ internal class TangemTech(
|
|||
|
||||
private fun createHeaders() = buildMap {
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
|
||||
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
|
||||
}
|
||||
}
|
||||
|
|
@ -51,19 +51,11 @@ interface TangemTechApi {
|
|||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("referral/{walletId}")
|
||||
suspend fun getReferralStatus(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("walletId") walletId: String,
|
||||
): ReferralResponse
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse
|
||||
|
||||
/** Make user referral, requires [StartReferralBody] */
|
||||
@POST("referral")
|
||||
suspend fun startReferral(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Body startReferralBody: StartReferralBody,
|
||||
): ReferralResponse
|
||||
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse
|
||||
|
||||
@GET("quotes")
|
||||
suspend fun getQuotes(
|
||||
|
|
@ -79,55 +71,35 @@ interface TangemTechApi {
|
|||
): ApiResponse<PromotionInfoResponse>
|
||||
|
||||
@GET("settings/{wallet_id}")
|
||||
suspend fun getUserTokensSettings(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<UserTokensSettingsResponse>
|
||||
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
|
||||
|
||||
@PUT("settings/{wallet_id}")
|
||||
suspend fun saveUserTokensSettings(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body userTokensSettings: UserTokensSettingsResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("user-network-account")
|
||||
suspend fun createUserNetworkAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Body body: CreateUserNetworkAccountBody,
|
||||
): ApiResponse<CreateUserNetworkAccountResponse>
|
||||
|
||||
@POST("account")
|
||||
suspend fun createUserTokensAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Body body: CreateUserTokensAccountBody,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}")
|
||||
suspend fun updateUserTokensAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("account_id") accountId: Int,
|
||||
@Body body: UpdateUserTokensAccountBody,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}/archive")
|
||||
suspend fun archiveUserTokensAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("account_id") accountId: Int,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}/unarchive")
|
||||
suspend fun restoreUserTokensAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("account_id") accountId: Int,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@GET("features")
|
||||
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,10 @@ import retrofit2.http.*
|
|||
interface TangemTechApiV2 {
|
||||
|
||||
@GET("user-tokens/{wallet_id}")
|
||||
suspend fun getUserTokens(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<UserTokensResponseV2>
|
||||
suspend fun getUserTokens(@Path("wallet_id") walletId: String): ApiResponse<UserTokensResponseV2>
|
||||
|
||||
@PUT("user-tokens/{wallet_id}")
|
||||
suspend fun saveUserTokens(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body userTokens: UserTokensResponseV2,
|
||||
): ApiResponse<Unit>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.Express
|
||||
import com.tangem.datasource.api.common.config.StakeKit
|
||||
|
|
@ -36,5 +37,6 @@ internal object ApiConfigsModule {
|
|||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemTechConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemTech(appVersionProvider)
|
||||
fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig =
|
||||
TangemTech(appVersionProvider, authProvider)
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import android.os.Build
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader.checkHeaderValueOrEmpty
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Presentation of request header
|
||||
|
|
@ -24,5 +28,24 @@ sealed class RequestHeader(vararg pairs: Pair<String, Provider<String>>) {
|
|||
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
|
||||
"version" to Provider(appVersionProvider::versionName),
|
||||
"platform" to Provider { "android" },
|
||||
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to Provider {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
)
|
||||
|
||||
/**
|
||||
* Use it to avoid crash in okhttp headers
|
||||
*/
|
||||
fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
val charCondition = c == '\t' || c in '\u0020'..'\u007e'
|
||||
if (!charCondition) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import android.os.Build
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE
|
||||
|
|
@ -18,16 +20,19 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
private val configManager = MockEnvironmentConfigStorage()
|
||||
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
|
||||
// Don't forget to add new config !!!
|
||||
private val API_CONFIGS = setOf(
|
||||
Express(configManager, expressAuthProvider, appVersionProvider),
|
||||
TangemTech(appVersionProvider),
|
||||
TangemTech(appVersionProvider, appAuthProvider),
|
||||
StakeKit(stakeKitAuthProvider),
|
||||
)
|
||||
|
||||
|
|
@ -46,6 +51,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
|
||||
every { expressAuthProvider.getRefCode() } returns EXPRESS_REF_CODE
|
||||
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
|
||||
every { appAuthProvider.getCardId() } returns APP_CARD_ID
|
||||
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -68,6 +75,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val EXPRESS_REF_CODE = "express_ref_code"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "app_public_key"
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
|
|
@ -118,6 +127,11 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
"refcode" to Provider { EXPRESS_REF_CODE },
|
||||
"version" to Provider { VERSION_NAME },
|
||||
"platform" to Provider { "android" },
|
||||
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to Provider {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -130,8 +144,15 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem-tech.com/v1/",
|
||||
headers = mapOf(
|
||||
"card_id" to Provider { APP_CARD_ID },
|
||||
"card_public_key" to Provider { APP_CARD_PUBLIC_KEY },
|
||||
"version" to Provider { VERSION_NAME },
|
||||
"platform" to Provider { "android" },
|
||||
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to Provider {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -150,5 +171,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
val charCondition = c == '\t' || c in '\u0020'..'\u007e'
|
||||
if (!charCondition) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemColorPalette.Dark6
|
||||
import com.tangem.core.ui.res.TangemColorPalette.Light2
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun NoteMigrationNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||
button ?: return
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemColorPalette.Dark6),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.heightIn(max = 146.dp),
|
||||
painter = painterResource(id = R.drawable.banner_note_migration),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 12.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
config.title?.let {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
color = TangemColorPalette.White,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = config.subtitle.resolveReference(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = TangemColorPalette.Light5,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
TangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = button.text.resolveReference(),
|
||||
icon = TangemButtonIconPosition.None,
|
||||
onClick = button.onClick,
|
||||
colors = TangemButtonColors(
|
||||
backgroundColor = Light2,
|
||||
contentColor = Dark6,
|
||||
disabledBackgroundColor = TangemTheme.colors.button.disabled,
|
||||
disabledContentColor = TangemTheme.colors.text.disabled,
|
||||
),
|
||||
enabled = true,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
showProgress = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 328)
|
||||
@Composable
|
||||
private fun NoteMigrationNotification_Preview() {
|
||||
TangemThemePreview {
|
||||
NoteMigrationNotification(
|
||||
NotificationConfig(
|
||||
title = stringReference("Discover Tangem Wallet"),
|
||||
subtitle = stringReference(
|
||||
"Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap. Link up to " +
|
||||
"three cards for a backup.",
|
||||
),
|
||||
iconResId = R.drawable.ic_empty_64,
|
||||
onCloseClick = { },
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = stringReference("Get now"),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
BIN
core/ui/src/main/res/drawable/banner_note_migration.webp
Normal file
BIN
core/ui/src/main/res/drawable/banner_note_migration.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
|
|
@ -70,14 +70,10 @@ internal class DefaultMarketsTokenRepository(
|
|||
|
||||
// we shouldn't infinitely retry on the first batch request
|
||||
val res = if (isFirstBatchFetching) {
|
||||
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
|
||||
requestCall()
|
||||
}
|
||||
catchListErrorAndSendEvent { requestCall() }
|
||||
} else {
|
||||
retryOnError(priority = true) {
|
||||
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
|
||||
requestCall()
|
||||
}
|
||||
catchListErrorAndSendEvent { requestCall() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +102,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
marketsApi = marketsApi,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onApiError = {
|
||||
analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent())
|
||||
analyticsEventHandler.send(createListErrorEvent(it).toEvent())
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -134,11 +130,9 @@ internal class DefaultMarketsTokenRepository(
|
|||
interval = interval.toRequestParam(),
|
||||
)
|
||||
|
||||
val result = catchApiErrorAndSendEvent(
|
||||
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
|
||||
tokenSymbol = tokenSymbol,
|
||||
),
|
||||
val result = catchDetailsErrorAndSendEvent(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
|
||||
tokenSymbol = tokenSymbol,
|
||||
) {
|
||||
response.getOrThrow()
|
||||
}
|
||||
|
|
@ -152,6 +146,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
analyticsEventHandler.send(
|
||||
MarketsDataAnalyticsEvent.ChartNullValuesError(
|
||||
requestPath = "coins/history",
|
||||
errorType = MarketsDataAnalyticsEvent.Type.Custom,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -165,18 +160,16 @@ internal class DefaultMarketsTokenRepository(
|
|||
tokenSymbol: String,
|
||||
) = withContext(dispatcherProvider.io) {
|
||||
val mappedTokenId = getTokenIdIfL2Network(tokenId)
|
||||
val response = marketsApi.getCoinsListCharts(
|
||||
coinIds = mappedTokenId,
|
||||
currency = fiatCurrencyCode,
|
||||
interval = interval.toRequestParam(),
|
||||
)
|
||||
|
||||
val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
|
||||
response.getOrThrow()[mappedTokenId] ?: error(
|
||||
val chart = catchListErrorAndSendEvent {
|
||||
marketsApi.getCoinsListCharts(
|
||||
coinIds = mappedTokenId,
|
||||
currency = fiatCurrencyCode,
|
||||
interval = interval.toRequestParam(),
|
||||
).getOrThrow()[mappedTokenId] ?: error(
|
||||
"No chart preview data for the token $mappedTokenId",
|
||||
)
|
||||
}
|
||||
|
||||
return@withContext TokenChartConverter.convert(
|
||||
interval = interval,
|
||||
value = chart,
|
||||
|
|
@ -186,6 +179,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
analyticsEventHandler.send(
|
||||
MarketsDataAnalyticsEvent.ChartNullValuesError(
|
||||
requestPath = "coins/history_preview",
|
||||
errorType = MarketsDataAnalyticsEvent.Type.Custom,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -198,19 +192,15 @@ internal class DefaultMarketsTokenRepository(
|
|||
tokenSymbol: String,
|
||||
languageCode: String,
|
||||
) = withContext(dispatcherProvider.io) {
|
||||
val response = marketsApi.getCoinMarketData(
|
||||
currency = fiatCurrencyCode,
|
||||
coinId = tokenId,
|
||||
language = languageCode,
|
||||
)
|
||||
|
||||
val result = catchApiErrorAndSendEvent(
|
||||
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
|
||||
tokenSymbol = tokenSymbol,
|
||||
),
|
||||
val result = catchDetailsErrorAndSendEvent(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
|
||||
tokenSymbol = tokenSymbol,
|
||||
) {
|
||||
response.getOrThrow()
|
||||
marketsApi.getCoinMarketData(
|
||||
currency = fiatCurrencyCode,
|
||||
coinId = tokenId,
|
||||
language = languageCode,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
val resultResponse = result.applyL2Compatibility(tokenId)
|
||||
|
|
@ -220,19 +210,16 @@ internal class DefaultMarketsTokenRepository(
|
|||
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
// for second markets iteration we should use extended api method with all required fields
|
||||
val response = tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyCode,
|
||||
coinIds = tokenId,
|
||||
fields = marketsQuoteFields.joinToString(separator = ","),
|
||||
)
|
||||
|
||||
val result = catchApiErrorAndSendEvent(
|
||||
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
|
||||
tokenSymbol = tokenSymbol,
|
||||
),
|
||||
val result = catchDetailsErrorAndSendEvent(
|
||||
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
|
||||
tokenSymbol = tokenSymbol,
|
||||
) {
|
||||
response.getOrThrow()
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyCode,
|
||||
coinIds = tokenId,
|
||||
fields = marketsQuoteFields.joinToString(separator = ","),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull()
|
||||
|
|
@ -282,15 +269,75 @@ internal class DefaultMarketsTokenRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun <T> catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T {
|
||||
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
|
||||
return catchErrorAndSendEvent(block, ::createListErrorEvent)
|
||||
}
|
||||
|
||||
private inline fun <T> catchDetailsErrorAndSendEvent(
|
||||
request: MarketsDataAnalyticsEvent.Details.Error.Request,
|
||||
tokenSymbol: String,
|
||||
block: () -> T,
|
||||
): T {
|
||||
return catchErrorAndSendEvent(block) { error ->
|
||||
createDetailsErrorEvent(error, request, tokenSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> catchErrorAndSendEvent(
|
||||
block: () -> T,
|
||||
createErrorEvent: (ApiResponseError) -> MarketsDataAnalyticsEvent,
|
||||
): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ApiResponseError.HttpException) {
|
||||
analyticsEventHandler.send(errorEvent.toEvent())
|
||||
throw e
|
||||
} catch (e: ApiResponseError.TimeoutException) {
|
||||
} catch (e: ApiResponseError) {
|
||||
val errorEvent = createErrorEvent(e)
|
||||
|
||||
analyticsEventHandler.send(errorEvent.toEvent())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun createListErrorEvent(error: ApiResponseError): MarketsDataAnalyticsEvent.List.Error {
|
||||
return createErrorEvent(error) { errorType, errorCode ->
|
||||
MarketsDataAnalyticsEvent.List.Error(
|
||||
errorType = errorType,
|
||||
errorCode = errorCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDetailsErrorEvent(
|
||||
error: ApiResponseError,
|
||||
request: MarketsDataAnalyticsEvent.Details.Error.Request,
|
||||
tokenSymbol: String,
|
||||
): MarketsDataAnalyticsEvent.Details.Error {
|
||||
return createErrorEvent(error) { errorType, errorCode ->
|
||||
MarketsDataAnalyticsEvent.Details.Error(
|
||||
errorType = errorType,
|
||||
errorCode = errorCode,
|
||||
request = request,
|
||||
tokenSymbol = tokenSymbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> createErrorEvent(
|
||||
error: ApiResponseError,
|
||||
createEvent: (MarketsDataAnalyticsEvent.Type, Int?) -> T,
|
||||
): T {
|
||||
return when (error) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code)
|
||||
}
|
||||
is ApiResponseError.TimeoutException -> {
|
||||
createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null)
|
||||
}
|
||||
is ApiResponseError.NetworkException -> {
|
||||
createEvent(MarketsDataAnalyticsEvent.Type.Network, null)
|
||||
}
|
||||
is ApiResponseError.UnknownException -> {
|
||||
createEvent(MarketsDataAnalyticsEvent.Type.Unknown, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ internal class MarketsBatchUpdateFetcher(
|
|||
private val marketsApi: TangemTechMarketsApi,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onApiError: () -> Unit,
|
||||
private val onApiError: (ApiResponseError) -> Unit,
|
||||
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
|
||||
|
|
@ -124,6 +124,7 @@ internal class MarketsBatchUpdateFetcher(
|
|||
analyticsEventHandler.send(
|
||||
MarketsDataAnalyticsEvent.ChartNullValuesError(
|
||||
requestPath = "coins/history_preview",
|
||||
errorType = MarketsDataAnalyticsEvent.Type.Custom,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -133,11 +134,11 @@ internal class MarketsBatchUpdateFetcher(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun <T> catchApiError(onError: () -> Unit, block: () -> T): T {
|
||||
private inline fun <T> catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ApiResponseError) {
|
||||
onError()
|
||||
onError(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,16 @@ sealed interface MarketsDataAnalyticsEvent {
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent {
|
||||
|
||||
data object Error : List(event = "Data Error")
|
||||
data class Error(
|
||||
val errorType: Type,
|
||||
val errorCode: Int? = null,
|
||||
) : List(
|
||||
event = "Data Error",
|
||||
params = buildMap {
|
||||
put("Error Type", errorType.value)
|
||||
errorCode?.let { put("Error Code", it.toString()) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Details(
|
||||
|
|
@ -20,12 +29,16 @@ sealed interface MarketsDataAnalyticsEvent {
|
|||
data class Error(
|
||||
val request: Request,
|
||||
val tokenSymbol: String,
|
||||
val errorType: Type,
|
||||
val errorCode: Int? = null,
|
||||
) : Details(
|
||||
event = "Data Error",
|
||||
params = mapOf(
|
||||
"Source" to request.source,
|
||||
"Token" to tokenSymbol,
|
||||
),
|
||||
params = buildMap {
|
||||
put("Source", request.source)
|
||||
put("Token", tokenSymbol)
|
||||
errorCode?.let { put("Error Code", it.toString()) }
|
||||
put("Error Type", errorType.value)
|
||||
},
|
||||
) {
|
||||
|
||||
enum class Request(val source: String) {
|
||||
|
|
@ -43,13 +56,25 @@ sealed interface MarketsDataAnalyticsEvent {
|
|||
|
||||
data class ChartNullValuesError(
|
||||
val requestPath: String,
|
||||
val errorType: Type,
|
||||
val errorCode: Int? = null,
|
||||
) : AnalyticsEvent(
|
||||
category = "Markets / Chart",
|
||||
event = "Data Error",
|
||||
params = mapOf("Request path" to requestPath),
|
||||
error = IllegalStateException(
|
||||
"Chart data contains null values from the API",
|
||||
),
|
||||
params = buildMap {
|
||||
put("Request path", requestPath)
|
||||
errorCode?.let { put("Error Code", it.toString()) }
|
||||
put("Error Type", errorType.value)
|
||||
put("Error Description", "Chart data contains null values from the API")
|
||||
},
|
||||
),
|
||||
MarketsDataAnalyticsEvent
|
||||
|
||||
enum class Type(val value: String) {
|
||||
Http("Http"),
|
||||
Timeout("Timeout"),
|
||||
Network("Network"),
|
||||
Custom("Custom"),
|
||||
Unknown("Unknown"),
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.blockchainsdk.utils.toCoinId
|
|||
import com.tangem.blockchainsdk.utils.toMigratedCointId
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.staking.converters.*
|
||||
import com.tangem.data.staking.converters.action.ActionStatusConverter
|
||||
|
|
@ -447,11 +448,20 @@ internal class DefaultStakingRepository(
|
|||
return@invokeOnExpire
|
||||
}
|
||||
|
||||
val result = stakeKitApi
|
||||
.getMultipleYieldBalances(availableCurrencies)
|
||||
.getOrThrow()
|
||||
val yieldBalances = safeApiCall(
|
||||
call = {
|
||||
stakeKitApi
|
||||
.getMultipleYieldBalances(availableCurrencies)
|
||||
.bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.e(it, "Unable to fetch yield balances")
|
||||
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
|
||||
emptySet()
|
||||
},
|
||||
)
|
||||
|
||||
stakingBalanceStore.store(userWalletId, result)
|
||||
stakingBalanceStore.store(userWalletId, yieldBalances)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ class YieldConverter(
|
|||
metadata = convertMetadata(value.metadata.asMandatory("metadata")),
|
||||
validators = value.validators.asMandatory("validators")
|
||||
.asSequence()
|
||||
.distinctBy { it.address }
|
||||
.filter { it.status == ValidatorStatusDTO.ACTIVE }
|
||||
.map { convertValidator(it) }
|
||||
.sortedByDescending { it.isStrategicPartner }
|
||||
.sortedByDescending { it.apr }
|
||||
.sortedByDescending { it.isStrategicPartner }
|
||||
.toImmutableList(),
|
||||
isAvailable = value.isAvailable.asMandatory("isAvailable"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ internal class DefaultQuotesRepository(
|
|||
.filterNotNull()
|
||||
.flatMapLatest { appCurrency ->
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh)
|
||||
quotesStore.get(currenciesIds).map(quotesConverter::convertSet)
|
||||
quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) }
|
||||
}
|
||||
.cancellable()
|
||||
.flowOn(dispatchers.io)
|
||||
|
|
@ -77,14 +77,15 @@ internal class DefaultQuotesRepository(
|
|||
|
||||
val quotes = quotesStore.getSync(currenciesIds)
|
||||
|
||||
quotesConverter.convertSet(quotes)
|
||||
quotesConverter.convert(currenciesIds to quotes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? {
|
||||
return withContext(dispatchers.io) {
|
||||
val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull()
|
||||
quote?.let { quotesConverter.convert(it) }
|
||||
val setOfCurrencyId = setOf(currencyId)
|
||||
val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull()
|
||||
quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.local.quote.model.StoredQuote
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuotesConverter : Converter<StoredQuote, Quote> {
|
||||
typealias QuotesConverterValue = Pair<Set<CryptoCurrency.ID>, Set<StoredQuote>>
|
||||
|
||||
override fun convert(value: StoredQuote): Quote {
|
||||
val (rawCurrencyId, responseQuote) = value
|
||||
internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
|
||||
|
||||
return Quote(
|
||||
override fun convert(value: QuotesConverterValue): Set<Quote> {
|
||||
val (setOfCurrencyId, setOfStoredQuote) = value
|
||||
return setOfCurrencyId.mapTo(hashSetOf()) { id ->
|
||||
setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId }
|
||||
?.let(::convertExistStoredQuote)
|
||||
?: Quote.Empty(id.rawCurrencyId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertExistStoredQuote(storedQuote: StoredQuote): Quote {
|
||||
val (rawCurrencyId, responseQuote) = storedQuote
|
||||
|
||||
return Quote.Value(
|
||||
rawCurrencyId = rawCurrencyId,
|
||||
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
|
||||
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ class GetCurrencyQuotesUseCase(
|
|||
currencyID: CryptoCurrency.ID,
|
||||
interval: PriceChangeInterval,
|
||||
refresh: Boolean,
|
||||
): Flow<Option<Quote>> {
|
||||
): Flow<Option<Quote.Value>> {
|
||||
return quotesRepository.getQuotesUpdates(
|
||||
currenciesIds = setOf(currencyID),
|
||||
refresh = refresh,
|
||||
).map { it.firstOrNull().toOption() }.catch { emit(None) }
|
||||
).map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }.catch { emit(None) }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,27 @@ package com.tangem.domain.tokens.model
|
|||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
|
||||
*
|
||||
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
|
||||
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
|
||||
* @property priceChange The price change for the cryptocurrency.
|
||||
*/
|
||||
data class Quote(
|
||||
val rawCurrencyId: String,
|
||||
val fiatRate: BigDecimal,
|
||||
val priceChange: BigDecimal,
|
||||
)
|
||||
sealed interface Quote {
|
||||
|
||||
val rawCurrencyId: String?
|
||||
|
||||
/**
|
||||
* Represents unknown financial information for a specific cryptocurrency.
|
||||
*
|
||||
* @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`.
|
||||
*/
|
||||
data class Empty(override val rawCurrencyId: String?) : Quote
|
||||
|
||||
/**
|
||||
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
|
||||
*
|
||||
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
|
||||
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
|
||||
* @property priceChange The price change for the cryptocurrency.
|
||||
*/
|
||||
data class Value(
|
||||
override val rawCurrencyId: String,
|
||||
val fiatRate: BigDecimal,
|
||||
val priceChange: BigDecimal,
|
||||
) : Quote
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.tokens.model.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -77,4 +78,43 @@ sealed class TokenScreenAnalyticsEvent(
|
|||
event = "Staking Clicked",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
class NoticeActionInactive(token: String, tokenAction: TokenAction, reason: String) : TokenScreenAnalyticsEvent(
|
||||
"Notice - Action Inactive",
|
||||
params = buildMap {
|
||||
put("Token", token)
|
||||
put("Action", tokenAction.action)
|
||||
if (reason.isNotEmpty()) {
|
||||
put("Reason", reason)
|
||||
}
|
||||
},
|
||||
) {
|
||||
sealed class TokenAction(val action: String) {
|
||||
data object BuyAction : TokenAction("Buy")
|
||||
data object SellAction : TokenAction("Sell")
|
||||
data object SwapAction : TokenAction("Swap")
|
||||
data object SendAction : TokenAction("Send")
|
||||
data object ReceiveAction : TokenAction("Receive")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNAVAILABLE = "Unavailable"
|
||||
private const val EMPTY = "Empty"
|
||||
private const val PENDING = "Pending"
|
||||
|
||||
fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String {
|
||||
return when (this) {
|
||||
is ScenarioUnavailabilityReason.BuyUnavailable -> UNAVAILABLE
|
||||
is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY
|
||||
ScenarioUnavailabilityReason.None -> ""
|
||||
is ScenarioUnavailabilityReason.NotExchangeable -> UNAVAILABLE
|
||||
is ScenarioUnavailabilityReason.NotSupportedBySellService -> UNAVAILABLE
|
||||
is ScenarioUnavailabilityReason.PendingTransaction -> PENDING
|
||||
is ScenarioUnavailabilityReason.StakingUnavailable -> UNAVAILABLE
|
||||
ScenarioUnavailabilityReason.UnassociatedAsset -> UNAVAILABLE
|
||||
ScenarioUnavailabilityReason.Unreachable -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,18 @@ internal class CurrencyStatusOperations(
|
|||
private val ignoreQuote: Boolean,
|
||||
) {
|
||||
|
||||
private val Quote?.fiatRate: BigDecimal?
|
||||
get() = when (this) {
|
||||
is Quote.Value -> this.fiatRate
|
||||
is Quote.Empty, null -> null
|
||||
}
|
||||
|
||||
private val Quote?.priceChange: BigDecimal?
|
||||
get() = when (this) {
|
||||
is Quote.Value -> this.priceChange
|
||||
is Quote.Empty, null -> null
|
||||
}
|
||||
|
||||
fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus())
|
||||
|
||||
private fun createStatus(): CryptoCurrencyStatus.Value {
|
||||
|
|
@ -74,7 +86,7 @@ internal class CurrencyStatusOperations(
|
|||
null
|
||||
}
|
||||
return when {
|
||||
ignoreQuote -> CryptoCurrencyStatus.NoQuote(
|
||||
quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
|
||||
amount = amount,
|
||||
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
|
||||
pendingTransactions = currentTransactions,
|
||||
|
|
@ -91,8 +103,7 @@ internal class CurrencyStatusOperations(
|
|||
networkAddress = status.address,
|
||||
yieldBalance = currentYieldBalance,
|
||||
)
|
||||
quote == null -> CryptoCurrencyStatus.Loading
|
||||
else -> CryptoCurrencyStatus.Loaded(
|
||||
quote is Quote.Value -> CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),
|
||||
fiatRate = quote.fiatRate,
|
||||
|
|
@ -102,6 +113,7 @@ internal class CurrencyStatusOperations(
|
|||
networkAddress = status.address,
|
||||
yieldBalance = currentYieldBalance,
|
||||
)
|
||||
else -> CryptoCurrencyStatus.Loading
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,65 +7,71 @@ import java.math.BigDecimal
|
|||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
internal object MockQuotes {
|
||||
|
||||
val quote1 = Quote(
|
||||
val quote1 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("1.23"),
|
||||
priceChange = BigDecimal("0.01"),
|
||||
)
|
||||
|
||||
val quote2 = Quote(
|
||||
val quote2 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("2.34"),
|
||||
priceChange = BigDecimal("-0.02"),
|
||||
)
|
||||
|
||||
val quote3 = Quote(
|
||||
val quote3 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("3.45"),
|
||||
priceChange = BigDecimal("0.03"),
|
||||
)
|
||||
|
||||
val quote4 = Quote(
|
||||
val quote4 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("4.56"),
|
||||
priceChange = BigDecimal("-0.04"),
|
||||
)
|
||||
|
||||
val quote5 = Quote(
|
||||
val quote5 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("5.67"),
|
||||
priceChange = BigDecimal("0.05"),
|
||||
)
|
||||
|
||||
val quote6 = Quote(
|
||||
val quote6 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("6.78"),
|
||||
priceChange = BigDecimal("-0.06"),
|
||||
)
|
||||
|
||||
val quote7 = Quote(
|
||||
val quote7 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("7.89"),
|
||||
priceChange = BigDecimal("0.07"),
|
||||
)
|
||||
|
||||
val quote8 = Quote(
|
||||
val quote8 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("8.90"),
|
||||
priceChange = BigDecimal("-0.08"),
|
||||
)
|
||||
|
||||
val quote9 = Quote(
|
||||
val quote9 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("9.01"),
|
||||
priceChange = BigDecimal("0.09"),
|
||||
)
|
||||
|
||||
val quote10 = Quote(
|
||||
val quote10 = Quote.Value(
|
||||
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
|
||||
fiatRate = BigDecimal("10.12"),
|
||||
priceChange = BigDecimal("-0.10"),
|
||||
)
|
||||
|
||||
val quotes = nonEmptySetOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10)
|
||||
val quote11 = Quote.Empty(null)
|
||||
val quote12 = Quote.Empty("null")
|
||||
|
||||
val quotes = nonEmptySetOf(
|
||||
quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10,
|
||||
quote11, quote12,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.domain.tokens.mock
|
||||
|
||||
import arrow.core.nonEmptyListOf
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
|
|
@ -140,20 +137,29 @@ internal object MockTokensStates {
|
|||
as? CryptoCurrencyAmountStatus.Loaded
|
||||
)?.value ?: BigDecimal.ZERO
|
||||
val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId }
|
||||
val fiatAmount = amount * quote.fiatRate
|
||||
|
||||
status.copy(
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
val value = when (quote) {
|
||||
is Quote.Empty -> CryptoCurrencyStatus.NoQuote(
|
||||
amount = status.value.amount!!,
|
||||
pendingTransactions = emptySet(),
|
||||
hasCurrentNetworkTransactions = false,
|
||||
networkAddress = requireNotNull(
|
||||
value = MockNetworks.verifiedNetworksStatuses.first { it.network == status.currency.network }.value as? NetworkStatus.Verified,
|
||||
).address,
|
||||
yieldBalance = null,
|
||||
)
|
||||
is Quote.Value -> CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatAmount = amount * quote.fiatRate,
|
||||
fiatRate = quote.fiatRate,
|
||||
priceChange = quote.priceChange,
|
||||
pendingTransactions = emptySet(),
|
||||
hasCurrentNetworkTransactions = false,
|
||||
networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address,
|
||||
yieldBalance = null,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
status.copy(value = value)
|
||||
}
|
||||
|
||||
val noQuotesTokensStatuses = loadedTokensStates.map { status ->
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.StartReferralBody
|
||||
import com.tangem.datasource.demo.DemoModeDatasource
|
||||
|
|
@ -24,7 +23,6 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
private val referralConverter: ReferralConverter,
|
||||
private val coroutineDispatcher: CoroutineDispatcherProvider,
|
||||
private val demoModeDatasource: DemoModeDatasource,
|
||||
private val authProvider: AuthProvider,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) : ReferralRepository {
|
||||
|
||||
|
|
@ -35,8 +33,6 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
return withContext(coroutineDispatcher.io) {
|
||||
referralConverter.convert(
|
||||
referralApi.getReferralStatus(
|
||||
cardPublicKey = authProvider.getCardPublicKey(),
|
||||
cardId = authProvider.getCardId(),
|
||||
walletId = walletId,
|
||||
),
|
||||
)
|
||||
|
|
@ -52,8 +48,6 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
return withContext(coroutineDispatcher.io) {
|
||||
referralConverter.convert(
|
||||
referralApi.startReferral(
|
||||
cardPublicKey = authProvider.getCardPublicKey(),
|
||||
cardId = authProvider.getCardId(),
|
||||
startReferralBody = StartReferralBody(
|
||||
walletId = walletId,
|
||||
networkId = networkId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.referral.di
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.demo.DemoModeDatasource
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -25,7 +24,6 @@ class ReferralRepositoryModule {
|
|||
referralConverter: ReferralConverter,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
demoModeDatasource: DemoModeDatasource,
|
||||
authProvider: AuthProvider,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
): ReferralRepository {
|
||||
return ReferralRepositoryImpl(
|
||||
|
|
@ -33,7 +31,6 @@ class ReferralRepositoryModule {
|
|||
referralConverter = referralConverter,
|
||||
coroutineDispatcher = coroutineDispatcherProvider,
|
||||
demoModeDatasource = demoModeDatasource,
|
||||
authProvider = authProvider,
|
||||
userWalletsStore = userWalletsStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2180,8 +2180,8 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote> {
|
||||
val set = ids.toSet().getQuotesOrEmpty(false)
|
||||
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote.Value> {
|
||||
val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance<Quote.Value>()
|
||||
|
||||
return ids
|
||||
.mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } }
|
||||
|
|
|
|||
|
|
@ -973,8 +973,12 @@ internal class StateBuilder(
|
|||
|
||||
fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder {
|
||||
val config = uiState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
|
||||
val permissionState = (uiState.permissionState as? GiveTxPermissionState.ReadyForRequest)?.copy(
|
||||
approveType = approveType,
|
||||
) ?: uiState.permissionState
|
||||
return if (config != null) {
|
||||
uiState.copy(
|
||||
permissionState = permissionState,
|
||||
bottomSheetConfig = uiState.bottomSheetConfig.copy(
|
||||
content = config.copy(
|
||||
data = config.data.copy(approveType = approveType),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
|
|
@ -16,7 +15,6 @@ data class SwapProcessDataState(
|
|||
// Amount from input
|
||||
val amount: String? = null,
|
||||
val approveDataModel: RequestApproveStateData? = null,
|
||||
val approveType: ApproveType? = null,
|
||||
val swapDataModel: SwapDataModel? = null,
|
||||
val selectedFee: TxFee? = null,
|
||||
val tokensDataState: TokensDataStateExpress? = null,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import arrow.core.getOrElse
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.bundle.unbundle
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
|
|
@ -530,10 +531,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapDataModel: SwapDataModel?,
|
||||
) {
|
||||
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
|
||||
dataState.copy(
|
||||
approveDataModel = permissionState.requestApproveData,
|
||||
approveType = dataState.approveType ?: ApproveType.UNLIMITED,
|
||||
)
|
||||
dataState.copy(approveDataModel = permissionState.requestApproveData)
|
||||
} else {
|
||||
dataState.copy(
|
||||
swapDataModel = swapDataModel,
|
||||
|
|
@ -673,9 +671,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
val approveDataModel = requireNotNull(dataState.approveDataModel) {
|
||||
"dataState.approveDataModel.spenderAddress shouldn't be null"
|
||||
}
|
||||
val approveType = requireNotNull(dataState.approveType?.toDomainApproveType()) {
|
||||
"uiState.permissionState should not be null"
|
||||
}
|
||||
val approveType =
|
||||
requireNotNull(uiState.permissionState.getApproveTypeOrNull()?.toDomainApproveType()) {
|
||||
"uiState.permissionState should not be null"
|
||||
}
|
||||
val feeForPermission = when (val fee = approveDataModel.fee) {
|
||||
TxFeeState.Empty -> {
|
||||
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
|
|
@ -1026,7 +1025,6 @@ internal class SwapViewModel @Inject constructor(
|
|||
onAmountSelected = { onAmountSelected(it) },
|
||||
onChangeApproveType = { approveType ->
|
||||
uiState = stateBuilder.updateApproveType(uiState, approveType)
|
||||
dataState = dataState.copy(approveType = approveType)
|
||||
},
|
||||
onClickFee = {
|
||||
val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL
|
||||
|
|
@ -1246,7 +1244,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
private fun sendPermissionApproveClickedEvent() {
|
||||
val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol
|
||||
val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol
|
||||
val approveType = dataState.approveType
|
||||
val approveType = uiState.permissionState.getApproveTypeOrNull()
|
||||
if (sendTokenSymbol != null && receiveTokenSymbol != null && approveType != null) {
|
||||
analyticsEventHandler.send(
|
||||
SwapEvents.ButtonPermissionApproveClicked(
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
|
@ -55,16 +55,22 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
.forEach { swapTransaction ->
|
||||
val toCryptoCurrency = swapTransaction.toCryptoCurrency
|
||||
val fromCryptoCurrency = swapTransaction.fromCryptoCurrency
|
||||
val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId
|
||||
val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId
|
||||
|
||||
swapTransaction.transactions.forEach { transaction ->
|
||||
val toAmount = transaction.toCryptoAmount
|
||||
val fromAmount = transaction.fromCryptoAmount
|
||||
val toFiatAmount = quotes.firstOrNull {
|
||||
it.rawCurrencyId == swapTransaction.toCryptoCurrency.id.rawCurrencyId
|
||||
}?.fiatRate?.multiply(toAmount)
|
||||
val fromFiatAmount = quotes.firstOrNull {
|
||||
it.rawCurrencyId == swapTransaction.fromCryptoCurrency.id.rawCurrencyId
|
||||
}?.fiatRate?.multiply(fromAmount)
|
||||
var toFiatAmount: BigDecimal? = null
|
||||
var fromFiatAmount: BigDecimal? = null
|
||||
quotes.forEach { quote ->
|
||||
if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) {
|
||||
toFiatAmount = quote.fiatRate.multiply(toAmount)
|
||||
}
|
||||
if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) {
|
||||
fromFiatAmount = quote.fiatRate.multiply(fromAmount)
|
||||
}
|
||||
}
|
||||
val timestamp = transaction.timestamp
|
||||
val notifications =
|
||||
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
|||
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
|
||||
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.transaction.error.AssociateAssetError
|
||||
|
|
@ -441,7 +442,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol))
|
||||
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.BuyAction,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
showErrorIfDemoModeOrElse {
|
||||
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
|
||||
|
|
@ -477,7 +485,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrency.symbol))
|
||||
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SendAction,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sendCurrency(status = cryptoCurrencyStatus ?: return)
|
||||
}
|
||||
|
|
@ -546,7 +561,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return
|
||||
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.ReceiveAction,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
|
||||
|
|
@ -566,7 +588,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = null,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
openStaking()
|
||||
}
|
||||
|
|
@ -598,7 +627,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol))
|
||||
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SellAction,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
showErrorIfDemoModeOrElse {
|
||||
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
|
||||
|
|
@ -615,7 +651,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrency.symbol))
|
||||
|
||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||
if (handleUnavailabilityReason(
|
||||
unavailabilityReason = unavailabilityReason,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SwapAction,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
appRouter.push(AppRoute.Swap(currencyFrom = cryptoCurrency, userWalletId = userWalletId))
|
||||
}
|
||||
|
|
@ -849,8 +892,21 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config)
|
||||
}
|
||||
|
||||
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
|
||||
private fun handleUnavailabilityReason(
|
||||
unavailabilityReason: ScenarioUnavailabilityReason,
|
||||
tokenSymbol: String,
|
||||
tokenAction: TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction?,
|
||||
): Boolean {
|
||||
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
|
||||
if (tokenAction != null) {
|
||||
analyticsEventsHandler.send(
|
||||
TokenScreenAnalyticsEvent.NoticeActionInactive(
|
||||
token = tokenSymbol,
|
||||
tokenAction = tokenAction,
|
||||
reason = unavailabilityReason.toReasonAnalyticsText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internalUiState.value = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason)
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
|
||||
data object BackupError : MainScreen(event = "Notice - Backup Error")
|
||||
|
||||
data object NotePromo : MainScreen(event = "Notice - Note Promo")
|
||||
|
||||
data object NotePromoButton : MainScreen(event = "Note Promo Button")
|
||||
|
||||
data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
|
||||
|
||||
data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
return warnings.mapNotNullTo(mutableSetOf(), ::getEvent)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getEvent(warning: WalletNotification): AnalyticsEvent? {
|
||||
return when (warning) {
|
||||
is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard
|
||||
|
|
@ -46,6 +47,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.NoteMigration -> MainScreen.NotePromo
|
||||
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
|
|||
buildList {
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver)
|
||||
addInformationalNotifications(cardTypesResolver, clickIntents)
|
||||
|
||||
addWarningNotifications(
|
||||
userWallet,
|
||||
|
|
@ -75,7 +75,17 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addInformationalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
private fun MutableList<WalletNotification>.addInformationalNotifications(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.NoteMigration(
|
||||
onClick = { clickIntents.onNoteMigrationButtonClick(NOTE_MIGRATION_URL) },
|
||||
),
|
||||
condition = cardTypesResolver.isTangemNote(),
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Informational.DemoCard,
|
||||
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
|
||||
|
|
@ -160,13 +170,17 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
|
|||
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
|
||||
if (condition) {
|
||||
add(element = element)
|
||||
if (element is WalletNotification.Critical || element is WalletNotification.Warning) {
|
||||
if (element is WalletNotification.Critical ||
|
||||
element is WalletNotification.Warning ||
|
||||
element is WalletNotification.NoteMigration
|
||||
) {
|
||||
readyForRateAppNotification = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10"
|
||||
const val MAX_REMAINING_SIGNATURES_COUNT = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
|
@ -143,7 +140,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
object DemoCard : Informational(
|
||||
data object DemoCard : Informational(
|
||||
title = resourceReference(id = R.string.warning_demo_mode_title),
|
||||
subtitle = resourceReference(id = R.string.warning_demo_mode_message),
|
||||
)
|
||||
|
|
@ -204,4 +201,16 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class NoteMigration(val onClick: () -> Unit) : WalletNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.wallet_promo_banner_title),
|
||||
subtitle = resourceReference(R.string.wallet_promo_banner_description),
|
||||
iconResId = R.drawable.banner_note_migration,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.wallet_promo_banner_button_title),
|
||||
onClick = onClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.common
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.NoteMigrationNotification
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.OkxPromoNotification
|
||||
import com.tangem.core.ui.components.notifications.RingPromoNotification
|
||||
|
|
@ -19,7 +19,6 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotification>, modifier: Modifier = Modifier) {
|
||||
items(
|
||||
items = configs,
|
||||
|
|
@ -31,16 +30,25 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
|||
is WalletNotification.SwapPromo -> {
|
||||
OkxPromoNotification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
is WalletNotification.RingPromo -> {
|
||||
RingPromoNotification(config = it.config, modifier = modifier.animateItemPlacement())
|
||||
RingPromoNotification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
is WalletNotification.NoteMigration -> {
|
||||
NoteMigrationNotification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
modifier = modifier.animateItem(),
|
||||
iconTint = when (it) {
|
||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ internal interface WalletWarningsClickIntents {
|
|||
fun onCloseRingPromoClick()
|
||||
|
||||
fun onSupportClick()
|
||||
|
||||
fun onNoteMigrationButtonClick(url: String)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -263,6 +265,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onNoteMigrationButtonClick(url: String) {
|
||||
analyticsEventHandler.send(MainScreen.NotePromoButton)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
router.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedUserWallet(): UserWallet? {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
return getUserWalletUseCase(userWalletId).getOrElse {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ markdownComposeView = "0.5.4"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "develop-868"
|
||||
tangemBlockchainSdk = "develop-871"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-403"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
|
||||
|
||||
internal class DefaultAccountCreator(
|
||||
private val authProvider: AuthProvider,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
) : AccountCreator {
|
||||
|
||||
|
|
@ -22,8 +20,6 @@ internal class DefaultAccountCreator(
|
|||
)
|
||||
return try {
|
||||
val response = tangemTechApi.createUserNetworkAccount(
|
||||
cardPublicKey = authProvider.getCardPublicKey(),
|
||||
cardId = authProvider.getCardId(),
|
||||
body = request,
|
||||
).getOrThrow()
|
||||
Result.Success(response.data.accountId)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggle
|
|||
import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader
|
||||
import com.tangem.blockchainsdk.store.DefaultRuntimeStore
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -45,14 +44,13 @@ internal object BlockchainSDKFactoryModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletManagerFactoryCreator(
|
||||
authProvider: AuthProvider,
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
blockchainSDKLogger: BlockchainSDKLogger,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): WalletManagerFactoryCreator {
|
||||
return WalletManagerFactoryCreator(
|
||||
accountCreator = DefaultAccountCreator(authProvider, tangemTechApi),
|
||||
accountCreator = DefaultAccountCreator(tangemTechApi),
|
||||
blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore),
|
||||
blockchainSDKLogger = blockchainSDKLogger,
|
||||
blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue