diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index bfe8750b2a..1f56b4bf2f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index d306ced544..1c1ea6340e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 16427253e8..c39150c2c2 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -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 ?: "" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 12e5639c12..62e564e8f0 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -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 diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index b88bfa47d4..402c43b957 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index c93ce9af1e..dd567e31f4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -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) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index d36820b605..99b5144126 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -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 @GET("settings/{wallet_id}") - suspend fun getUserTokensSettings( - @Header("card_public_key") cardPublicKey: String, - @Header("card_id") cardId: String, - @Path("wallet_id") walletId: String, - ): ApiResponse + suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse @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 @POST("user-network-account") suspend fun createUserNetworkAccount( - @Header("card_public_key") cardPublicKey: String, - @Header("card_id") cardId: String, @Body body: CreateUserNetworkAccountBody, ): ApiResponse @POST("account") suspend fun createUserTokensAccount( - @Header("card_public_key") cardPublicKey: String, - @Header("card_id") cardId: String, @Body body: CreateUserTokensAccountBody, ): ApiResponse @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 @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 + suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse @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 + suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse @GET("features") suspend fun getFeatures(): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt index 80db96faa7..164cd09362 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt @@ -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 + suspend fun getUserTokens(@Path("wallet_id") walletId: String): ApiResponse @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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index d900af3269..c5f506627e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -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) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index b6ed8b7645..5b9de76ce7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -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>) { 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 + } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 02ade46775..7c874508d8 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -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() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() +private val appAuthProvider = mockk() // 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 + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt new file mode 100644 index 0000000000..6b5f396f85 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt @@ -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 \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/banner_note_migration.webp b/core/ui/src/main/res/drawable/banner_note_migration.webp new file mode 100644 index 0000000000..6f4f2a0ec6 Binary files /dev/null and b/core/ui/src/main/res/drawable/banner_note_migration.webp differ diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 25703406d5..be0e41c584 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -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 catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T { + inline fun catchListErrorAndSendEvent(block: () -> T): T { + return catchErrorAndSendEvent(block, ::createListErrorEvent) + } + + private inline fun catchDetailsErrorAndSendEvent( + request: MarketsDataAnalyticsEvent.Details.Error.Request, + tokenSymbol: String, + block: () -> T, + ): T { + return catchErrorAndSendEvent(block) { error -> + createDetailsErrorEvent(error, request, tokenSymbol) + } + } + + private inline fun 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 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) + } + } + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index f1a92028fb..6a9952d71f 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -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, TokenMarketUpdateRequest> { override suspend fun BatchUpdateFetcher.UpdateContext>.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 catchApiError(onError: () -> Unit, block: () -> T): T { + private inline fun catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T { return try { block() } catch (e: ApiResponseError) { - onError() + onError(e) throw e } } diff --git a/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt index 1c131a3cd4..e0d27728a0 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt @@ -9,7 +9,16 @@ sealed interface MarketsDataAnalyticsEvent { params: Map = 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"), + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 00a9d68849..be7dc4d12e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -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) }, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 6d6b14b10b..431bf32bf5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -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"), ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 13e1c3e368..a680263464 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -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() } } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index 85c903ceec..79af4417b7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -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 { +typealias QuotesConverterValue = Pair, Set> - override fun convert(value: StoredQuote): Quote { - val (rawCurrencyId, responseQuote) = value +internal class QuotesConverter : Converter> { - return Quote( + override fun convert(value: QuotesConverterValue): Set { + 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), diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index 1ec184fbdc..0a8d0d931d 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -15,10 +15,10 @@ class GetCurrencyQuotesUseCase( currencyID: CryptoCurrency.ID, interval: PriceChangeInterval, refresh: Boolean, - ): Flow> { + ): Flow> { return quotesRepository.getQuotesUpdates( currenciesIds = setOf(currencyID), refresh = refresh, - ).map { it.firstOrNull().toOption() }.catch { emit(None) } + ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 3db9280b76..d54b9a2342 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -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, -) \ No newline at end of file +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 +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index c661b761bd..d42f2f31b5 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -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 -> "" + } + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index ff31ccc64a..3e4f3a77ae 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -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 } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 68de5e729a..002f80eae8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -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, + ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 45c2f537d4..a9d704b56f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -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 -> diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 63b2305951..c0c257c1c9 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -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, diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 23164dbe82..6c0f5e9c47 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -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, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 4072c1736a..9f48919bec 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2180,8 +2180,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { - val set = ids.toSet().getQuotesOrEmpty(false) + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance() return ids .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 19a49d15b2..620ca41a28 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 8a505fb148..b35cb05405 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 8250eaf93b..b3360ff214 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 2ec87f5ed8..fc0cfa0541 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index a83d17ed6f..b96f8a62d8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index bfb7efaf45..39d99aaf63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -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") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index fecbf47463..89d0467012 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index c502e65890..81e435fa5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -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.addInformationalNotifications(cardTypesResolver: CardTypesResolver) { + private fun MutableList.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.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 } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 2741cc56ca..feb796546d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -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, + ), + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 39dbbb2618..de41263c1e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -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, modifier: Modifier = Modifier) { items( items = configs, @@ -31,16 +30,25 @@ internal fun LazyListScope.notifications(configs: ImmutableList { 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 2d434ac248..16ef0fd1a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -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 { diff --git a/gradle.properties b/gradle.properties index c4b0b8ac32..a312f8c1a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7e0c09dcdd..0f18d4e68c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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 ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt index be9daa7448..c32e73a545 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt @@ -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) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 6ef9eb792b..de38a2241f 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -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),