Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-24 17:46:40 +03:00
commit 2baa5365b4
94 changed files with 705 additions and 1924 deletions

@ -1 +1 @@
Subproject commit 328c13826f7e52ba783db1ba39de5b17b1b79bc5
Subproject commit 5b216eeca70af8b9fa7b266d4d0c0f87f0c042c8

View file

@ -718,6 +718,26 @@
"networkId": "sei-network/test"
}
]
},
{
"id": "energy-web-chain",
"name": "Energy Web Chain",
"symbol": "EWT",
"networks": [
{
"networkId": "energy-web-chain/test"
}
]
},
{
"id": "energy-web-x",
"name": "Energy Web X",
"symbol": "EWT",
"networks": [
{
"networkId": "energy-web-x/test"
}
]
}
]
}

View file

@ -24,17 +24,17 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
@ -106,8 +106,6 @@ interface ApplicationEntryPoint {
fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase
fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener
@ -116,7 +114,9 @@ interface ApplicationEntryPoint {
fun getAppRouter(): AppRouter
fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles
fun getTangemAppLogger(): TangemAppLoggerInitializer
fun getHomeFeatureToggles(): HomeFeatureToggles
fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase
}

View file

@ -65,7 +65,6 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -78,7 +77,6 @@ import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
@ -539,11 +537,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
} else {
lifecycleScope.launch {
val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
val isPushPermissionEnabled = toggles.isPushNotificationsEnabled
val shouldShowTos = !cardRepository.isTangemTOSAccepted() && isPushPermissionEnabled
val wasPushInitiallyAsked = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val shouldShowInitialPush = wasPushInitiallyAsked && isPushPermissionEnabled
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val shouldShowInitialPush = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)

View file

@ -37,14 +37,13 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
@ -60,6 +59,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -174,9 +174,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles()
private val urlOpener
get() = entryPoint.getUrlOpener()
@ -186,11 +183,14 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val appRouter: AppRouter
get() = entryPoint.getAppRouter()
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles
get() = entryPoint.getPushNotificationsFeatureToggles()
private val tangemAppLoggerInitializer: TangemAppLoggerInitializer
get() = entryPoint.getTangemAppLogger()
private val homeFeatureToggles: HomeFeatureToggles
get() = entryPoint.getHomeFeatureToggles()
private val getUserCountryUseCase: GetUserCountryUseCase
get() = entryPoint.getGetUserCountryCodeUseCase()
// endregion
override fun onCreate() {
@ -268,11 +268,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener,
shareManager = shareManager,
appRouter = appRouter,
pushNotificationsFeatureToggles = pushNotificationsFeatureToggles,
homeFeatureToggles = homeFeatureToggles,
getUserCountryUseCase = getUserCountryUseCase,
),
),
)

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.ui.details.SocialNetwork
/**
[REDACTED_AUTHOR]
@ -14,20 +13,8 @@ sealed class Settings(
) : AnalyticsEvent(category, event, params, error) {
class ScreenOpened : Settings(event = "Settings Screen Opened")
class ButtonChat : Settings(event = "Button - Chat")
class ButtonSendFeedback : Settings(event = "Button - Send Feedback")
class ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session")
class ButtonStopWalletConnectSession : Settings(event = "Button - Stop Wallet Connect Session")
class ButtonCardSettings : Settings(event = "Button - Card Settings")
class ButtonAppSettings : Settings(event = "Button - App Settings")
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
class ButtonWalletConnect : Settings(event = "Button - Wallet Connect")
object ScanNewCard : Settings(event = "Button - Scan New Card")
class ButtonSocialNetwork(network: SocialNetwork) : Settings(
event = "Button - Social Network",
params = mapOf("Network" to network.id),
)
sealed class CardSettings(
event: String,
@ -63,8 +50,6 @@ sealed class Settings(
error = error,
)
class AccessCodeRecoveryButton : CardSettings("Button - Access Code Recovery")
class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings(
event = "Access Code Recovery Changed",
params = mapOf(status.key to status.value),

View file

@ -3,7 +3,6 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
@ -28,7 +27,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),

View file

@ -10,8 +10,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
@ -43,7 +41,6 @@ data class AppState(
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
@ -75,7 +72,6 @@ data class AppState(
OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,

View file

@ -92,21 +92,24 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
scope.launch { exchangeManager.update() }
}
is GlobalAction.FetchUserCountry -> {
scope.launch {
// TODO("After adding DI") get dependencies by DI
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
.onSuccess {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
)
}
.onFailure {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
scope.launch {
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
.onSuccess {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
)
}
.onFailure {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
}
}
}
}

View file

@ -2,12 +2,9 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -86,10 +83,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> {
store.dispatchOnMain(HomeAction.UpdateCountryCode(action.countryCode))
globalState.copy(
userCountryCode = action.countryCode,
)
globalState.copy(userCountryCode = action.countryCode)
}
else -> globalState
}

View file

@ -23,6 +23,7 @@ data class GlobalState(
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
@Deprecated("Use GetUserCountryCodeUseCase instead")
val userCountryCode: String? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType

View file

@ -10,6 +10,8 @@ import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
import dagger.Module
@ -212,4 +214,16 @@ internal object SettingsDomainModule {
return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository)
}
// endregion
@Provides
@Singleton
fun provideFetchUserCountryCodeUseCase(settingsRepository: SettingsRepository): FetchUserCountryUseCase {
return FetchUserCountryUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideGetUserCountryCodeUseCase(settingsRepository: SettingsRepository): GetUserCountryUseCase {
return GetUserCountryUseCase(settingsRepository)
}
}

View file

@ -13,8 +13,6 @@ import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -62,7 +60,6 @@ class TapWalletManager(
updateConfigManager(scanResponse)
withMainContext {
// Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
@ -18,9 +17,6 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
@ -102,6 +98,8 @@ internal object LegacyScanProcessor {
}
}
// TODO: [REDACTED_JIRA]
@Suppress("UnusedPrivateMember")
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
@ -109,7 +107,6 @@ internal object LegacyScanProcessor {
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {
nextHandler(scanResponse)
@ -120,23 +117,9 @@ internal object LegacyScanProcessor {
withContext(Dispatchers.Main.immediate) {
disclaimerWillShow()
store.dispatch(
DisclaimerAction.Show(
from = DisclaimerSource.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main.immediate) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main.immediate) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
)
store.dispatchNavigationAction {
push(AppRoute.Disclaimer(isTosAccepted = false))
}
}
}
}

View file

@ -59,11 +59,7 @@ internal object UseCaseScanProcessor {
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
val pushNotificationsToggles =
store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
if (pushNotificationsToggles.isPushNotificationsEnabled) {
add(DisclaimerChain(store, disclaimerWillShow))
}
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}

View file

@ -1,21 +1,15 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.left
import arrow.core.right
import com.tangem.common.routing.AppRoute
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import kotlinx.coroutines.suspendCancellableCoroutine
import org.rekotlin.Store
import kotlin.coroutines.resume
/**
* Handles disclaimer display after the card scanning operation.
@ -40,31 +34,13 @@ internal class DisclaimerChain(
previousChainResult.right()
} else {
disclaimerWillShow()
showDisclaimer(disclaimer, previousChainResult)
}
}
private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
// TODO: [REDACTED_JIRA]
store.dispatchNavigationAction {
push(route = AppRoute.Disclaimer(isTosAccepted = false))
}
return suspendCancellableCoroutine { continuation ->
store.dispatchOnMain(
DisclaimerAction.Show(
from = DisclaimerSource.Home,
callback = DisclaimerCallback(
onAccept = {
if (continuation.isActive) {
continuation.resume(response.right())
}
},
onDismiss = {
if (continuation.isActive) {
continuation.resume(ScanChainException.DisclaimerWasCanceled.left())
}
},
),
),
)
previousChainResult.right()
}
}
}

View file

@ -239,9 +239,8 @@ class WalletConnectSdkHelper {
private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? {
val dataToSign = EthereumUtils.buildTransactionToSign(
transactionData = data.transaction,
nonce = null,
blockchain = data.walletManager.wallet.blockchain,
) ?: return null
)
val command = SignHashCommand(
hash = dataToSign.hash,

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
@ -14,15 +13,6 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
data object Success : DetailsAction()
data class Error(val error: TextReference?) : DetailsAction()
}
data object DismissError : DetailsAction()
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,

View file

@ -2,36 +2,24 @@ package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
@ -41,7 +29,6 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware {
private val appSettingsMiddleware = AppSettingsMiddleware()
@ -62,7 +49,6 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
}
}
@ -278,95 +264,4 @@ class DetailsMiddleware {
}
}
}
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy()
// Update access code policy for access code saving when a card was scanned
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = shouldSaveAccessCodes)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsSource = CoreAnalyticsParam.ScreensSources.Settings,
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
},
disclaimerWillShow = {
store.dispatchNavigationAction(AppRouter::pop)
},
onSuccess = { scanResponse ->
createUserWallet(scanResponse)
.doOnSuccess {
saveUserWalletAndPopBackToWalletScreen(
userWallet = it,
prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode,
)
}
.doOnFailure { error ->
Timber.e(error, "Unable to create user wallet")
handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode)
}
},
onFailure = { error ->
Timber.e(error, "Unable to scan card")
handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode)
},
)
}
private suspend fun createUserWallet(scanResponse: ScanResponse): CompletionResult<UserWallet> {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build()
return if (userWallet != null) {
CompletionResult.Success(userWallet)
} else {
CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
}
}
private suspend fun saveUserWalletAndPopBackToWalletScreen(
userWallet: UserWallet,
prevUseBiometricsForAccessCode: Boolean,
) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
.doOnFailure { error ->
if (error is UserWalletsListError.WalletAlreadySaved) {
userWalletsListManager.select(userWallet.walletId)
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
Timber.e(error, "Unable to create user wallet")
handleError(error, prevUseBiometricsForAccessCode)
}
}
}
private suspend fun handleError(error: TangemError, prevUseBiometricsForAccessCode: Boolean) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
// Rollback policy if card saving was failed
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference()))
}
private fun TangemError.toTextReference(): TextReference? {
if (silent) return null
return messageResId?.let(::resourceReference) ?: stringReference(customMessage)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -21,7 +20,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
val detailsState = state.detailsState
return when (action) {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state)
handlePrepareScreen(action)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
@ -31,26 +30,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency,
),
)
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
isScanningInProgress = true,
)
is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy(
isScanningInProgress = false,
error = action.error,
)
is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy(
isScanningInProgress = false,
)
is DetailsAction.DismissError -> detailsState.copy(
error = null,
)
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
return DetailsState(
scanResponse = action.scanResponse,
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking {
tangemSdkManager.checkCanUseBiometry()

View file

@ -1,27 +1,16 @@
package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.StateType
data class DetailsState(
@Deprecated("Delete after onboarding refactoring")
val scanResponse: ScanResponse? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
val error: TextReference? = null,
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,

View file

@ -11,7 +11,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -45,7 +44,6 @@ internal class AppSettingsViewModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val detailsFeatureToggles: DetailsFeatureToggles,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
@ -62,9 +60,7 @@ internal class AppSettingsViewModel @Inject constructor(
init {
bootstrapAppCurrencyUpdates()
if (detailsFeatureToggles.isRedesignEnabled) {
bootstrapBiometricsUpdates()
}
bootstrapBiometricsUpdates()
subscribeToStoreChanges()
sendItemsAnalytics()

View file

@ -1,71 +0,0 @@
package com.tangem.tap.features.details.ui.details
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var walletsRepository: WalletsRepository
@Inject
lateinit var userWalletsListManager: UserWalletsListManager
private lateinit var detailsViewModel: DetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
detailsViewModel = DetailsViewModel(
store = store,
walletsRepository = walletsRepository,
userWalletsListManager = userWalletsListManager,
)
Analytics.send(Settings.ScreenOpened())
}
@Composable
override fun ScreenContent(modifier: Modifier) {
DetailsScreen(
modifier = modifier,
state = detailsViewModel.detailsScreenState.value,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
detailsViewModel.detailsScreenState.value = detailsViewModel.updateState(state)
}
}

View file

@ -1,288 +0,0 @@
package com.tangem.tap.features.details.ui.details
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
val snackbarHostState = remember { SnackbarHostState() }
SettingsScreensScaffold(
modifier = modifier,
snackbarHostState = snackbarHostState,
content = { Content(state = state) },
onBackClick = onBackClick,
)
ShowSnackbarIfNeeded(
snackbarHostState = snackbarHostState,
messageEvent = state.showSnackbar,
)
}
@Composable
private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
ScreenTitle(titleRes = R.string.details_title)
SpacerH(height = TangemTheme.dimens.spacing36)
SettingsItems(
items = state.elements,
)
SpacerHMax()
TangemSocialAccounts(
links = state.tangemLinks,
onSocialNetworkClick = state.onSocialNetworkClick,
)
SpacerH(height = TangemTheme.dimens.spacing12)
TangemAppVersion(
appNameRes = state.appNameRes,
version = state.tangemVersion,
)
SpacerH(height = TangemTheme.dimens.spacing16)
}
}
}
@Composable
private fun SettingsItems(items: List<SettingsItem>) {
items.forEach { item ->
if (item.isLarge) {
LargeDetailsItem(item)
} else {
DetailsItem(item)
}
}
}
@Composable
private fun LargeDetailsItem(item: SettingsItem) {
Row(
modifier = Modifier
.clickable(onClick = item.onClick)
.padding(horizontal = TangemTheme.dimens.spacing20)
.heightIn(min = TangemTheme.dimens.size84)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
verticalAlignment = Alignment.CenterVertically,
) {
if (item.showProgress) {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size24),
color = TangemTheme.colors.icon.informative,
)
} else {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = item.iconResId),
contentDescription = item.title.resolveReference(),
tint = TangemColorPalette.Azure,
)
}
Column(
modifier = Modifier.heightIn(min = TangemTheme.dimens.size56),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(
space = TangemTheme.dimens.spacing4,
alignment = Alignment.CenterVertically,
),
) {
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
if (item.subtitle != null) {
Text(
text = item.subtitle.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private fun DetailsItem(item: SettingsItem) {
Row(
modifier = Modifier
.clickable(enabled = !item.showProgress, onClick = item.onClick)
.padding(horizontal = TangemTheme.dimens.spacing20)
.heightIn(min = TangemTheme.dimens.size56)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
verticalAlignment = Alignment.CenterVertically,
) {
if (item.showProgress) {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size24),
color = TangemTheme.colors.icon.informative,
)
} else {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = item.iconResId),
contentDescription = item.title.resolveReference(),
tint = TangemTheme.colors.icon.secondary,
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceAround,
) {
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
if (item.subtitle != null) {
Text(
text = item.subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8),
) {
items(links) {
val onClick = remember(it) {
{ onSocialNetworkClick(it) }
}
IconButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.size(TangemTheme.dimens.size32),
onClick = onClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = it.network.iconRes),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}
}
@Composable
private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent<TextReference>) {
var message: TextReference? by remember { mutableStateOf(value = null) }
val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference())
LaunchedEffect(resolvedMessage) {
resolvedMessage?.let {
snackbarHostState.showSnackbar(it)
}
}
EventEffect(messageEvent) {
message = it
}
}
@Composable
private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
text = "${stringResource(id = appNameRes)} $version",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 900)
@Preview(showBackground = true, widthDp = 360, heightDp = 900, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DetailsScreenPreview(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) {
TangemThemePreview {
DetailsScreen(param, onBackClick = {})
}
}
private class DetailsScreenStateProvider : CollectionPreviewParameterProvider<DetailsScreenState>(
collection = buildList {
DetailsScreenState(
elements = buildList {
SettingsItem.WalletConnect({}).let(::add)
SettingsItem.AddWallet(showProgress = false, {}).let(::add)
SettingsItem.LinkMoreCards({}).let(::add)
SettingsItem.CardSettings({}).let(::add)
SettingsItem.AppSettings({}).let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat({}).let(::add)
SettingsItem.SendFeedback({}).let(::add)
SettingsItem.ReferralProgram({}).let(::add)
SettingsItem.TermsOfService({}).let(::add)
}.toImmutableList(),
tangemLinks = TangemSocialAccounts.accountsEn,
tangemVersion = "Tangem 2.14.12 (343)",
showSnackbar = consumedEvent(),
onSocialNetworkClick = {},
).let(::add)
DetailsScreenState(
elements = buildList {
SettingsItem.WalletConnect({}).let(::add)
SettingsItem.AddWallet(showProgress = true, {}).let(::add)
SettingsItem.CardSettings({}).let(::add)
SettingsItem.AppSettings({}).let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat({}).let(::add)
SettingsItem.SendFeedback({}).let(::add)
SettingsItem.TermsOfService({}).let(::add)
}.toImmutableList(),
tangemLinks = TangemSocialAccounts.accountsRu,
tangemVersion = "Tangem 2.14.12 (343)",
showSnackbar = consumedEvent(),
onSocialNetworkClick = {},
).let(::add)
},
)
// endregion Preview

View file

@ -1,162 +0,0 @@
package com.tangem.tap.features.details.ui.details
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
internal data class DetailsScreenState(
val elements: ImmutableList<SettingsItem>,
val tangemLinks: ImmutableList<SocialNetworkLink>,
val tangemVersion: String,
val showSnackbar: StateEvent<TextReference>,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
) {
val appNameRes: Int = R.string.tangem_app_name
}
@Immutable
internal sealed class SettingsItem(
@DrawableRes val iconResId: Int,
val title: TextReference,
val subtitle: TextReference? = null,
val isLarge: Boolean = false,
) {
abstract val onClick: () -> Unit
open val showProgress: Boolean = false
data class WalletConnect(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_wallet_connect_24,
title = resourceReference(R.string.wallet_connect_title),
subtitle = resourceReference(R.string.wallet_connect_subtitle),
isLarge = true,
)
data class AddWallet(
override val showProgress: Boolean,
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = resourceReference(R.string.user_wallet_list_add_button),
)
data class ScanWallet(
override val showProgress: Boolean,
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = resourceReference(R.string.scan_card_settings_button),
)
data class LinkMoreCards(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_more_cards_24,
title = resourceReference(R.string.details_row_title_create_backup),
)
data class CardSettings(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_card_settings_24,
title = resourceReference(R.string.card_settings_title),
)
data class AppSettings(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_settings,
title = resourceReference(R.string.app_settings_title),
)
data class Chat(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_chat_24,
title = resourceReference(R.string.details_chat),
)
data class SendFeedback(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_comment_24,
title = resourceReference(R.string.details_row_title_contact_to_support),
)
data class ReferralProgram(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_add_friends_24,
title = resourceReference(R.string.details_referral_title),
)
data class TermsOfService(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_text_24,
title = resourceReference(R.string.disclaimer_title),
)
data class TesterMenu(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_alert_24,
title = resourceReference(R.string.tester_menu),
)
}
@Immutable
internal data class SocialNetworkLink(
val network: SocialNetwork,
val url: String,
)
internal sealed class EventError {
object Empty : EventError()
}
sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter_24)
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram_24)
object Discord : SocialNetwork("Discord", R.drawable.ic_discord_24)
object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit_24)
object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram_24)
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github_24)
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook_24)
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin_24)
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube_24)
}
internal object TangemSocialAccounts {
val accountsEn: ImmutableList<SocialNetworkLink> = persistentListOf(
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://www.instagram.com/tangemwallet"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://www.facebook.com/tangemwallet"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem_official"),
)
val accountsRu: ImmutableList<SocialNetworkLink> = persistentListOf(
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://www.instagram.com/tangemwallet"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://www.facebook.com/tangemwallet"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem_official"),
)
}

View file

@ -1,215 +0,0 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.scope
import com.tangem.wallet.BuildConfig
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import timber.log.Timber
internal class DetailsViewModel(
private val store: Store<AppState>,
private val walletsRepository: WalletsRepository,
private val userWalletsListManager: UserWalletsListManager,
) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
private set
init {
bootstrapScreenState()
}
fun updateState(state: DetailsState): DetailsScreenState {
return DetailsScreenState(
elements = createSettingsItems(state),
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
showSnackbar = triggerErrorSnackbarIfNeeded(state.error),
onSocialNetworkClick = ::handleSocialNetworkClick,
)
}
private fun createSettingsItems(state: DetailsState): ImmutableList<SettingsItem> {
val scanResponse = state.scanResponse ?: return persistentListOf()
val cardTypesResolver = scanResponse.cardTypesResolver
return buildList {
SettingsItem.WalletConnect(::navigateToWalletConnect)
.takeIf { cardTypesResolver.isMultiwalletAllowed() }
?.let(::add)
SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
.takeIf { state.appSettingsState.saveWallets }
?.let(::add)
SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
.takeUnless { state.appSettingsState.saveWallets }
?.let(::add)
SettingsItem.LinkMoreCards(::linkMoreCards)
.takeIf { state.createBackupAllowed }
?.let(::add)
SettingsItem.CardSettings(::navigateToCardSettings)
.let(::add)
SettingsItem.AppSettings(::navigateToAppSettings)
.let(::add)
SettingsItem.SendFeedback(::sendFeedback)
.let(::add)
SettingsItem.ReferralProgram(::navigateToReferralProgram)
.takeIf { cardTypesResolver.isTangemWallet() }
?.let(::add)
SettingsItem.TermsOfService(::navigateToToS)
.let(::add)
SettingsItem.TesterMenu(::navigateToTesterMenu)
.takeIf { BuildConfig.TESTER_MENU_ENABLED }
?.let(::add)
}.toImmutableList()
}
private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent<TextReference> {
return if (text == null) {
consumedEvent()
} else {
triggeredEvent(text) {
store.dispatch(DetailsAction.DismissError)
}
}
}
private fun getTangemAppVersion(): String {
val versionCode: Int = BuildConfig.VERSION_CODE
val versionName: String = BuildConfig.VERSION_NAME
return "$versionName ($versionCode)"
}
private fun navigateToTesterMenu() {
store.dispatchNavigationAction {
push(AppRoute.TesterMenu)
}
}
private fun navigateToToS() {
store.dispatchOnMain(DisclaimerAction.Show(DisclaimerSource.Details))
}
private fun navigateToReferralProgram() {
val userWallet = userWalletsListManager.selectedUserWalletSync
?: error("Selected wallet must be not null")
store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) }
}
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
store.dispatchOnMain(
GlobalAction.SendEmail(
feedbackData = FeedbackEmail(),
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())
store.dispatchNavigationAction { push(AppRoute.AppSettings) }
}
private fun navigateToCardSettings() {
val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId
?: error("UserWalletId must be not null")
Analytics.send(Settings.ButtonCardSettings())
store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) }
}
private fun linkMoreCards() {
Analytics.send(Settings.ButtonCreateBackup())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to backup wallet, no user wallet selected")
return
}
val scanResponse = selectedUserWallet.scanResponse
Analytics.addContext(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
}
private fun scanAndSaveUserWallet() {
Analytics.send(Settings.ScanNewCard)
store.dispatchOnMain(DetailsAction.ScanAndSaveUserWallet)
}
private fun navigateToWalletConnect() {
Analytics.send(Settings.ButtonWalletConnect())
store.dispatchNavigationAction { push(AppRoute.WalletConnectSessions) }
}
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
Analytics.send(Settings.ButtonSocialNetwork(link.network))
store.dispatchOpenUrl(link.url)
}
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {
val locale = LocaleRegionProvider().getRegion()
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
TangemSocialAccounts.accountsRu
} else {
TangemSocialAccounts.accountsEn
}
}
private fun bootstrapScreenState() {
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
.onEach { selectedUserWallet ->
store.dispatchWithMain(
DetailsAction.PrepareScreen(
scanResponse = selectedUserWallet.scanResponse,
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
),
)
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.details.ui.resetcard
internal sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}

View file

@ -22,7 +22,6 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.details.redux.ResetCardDialog
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.store

View file

@ -24,12 +24,6 @@ abstract class BaseDisclaimer(
override suspend fun isAccepted(): Boolean = dataProvider.isAccepted()
}
class DummyDisclaimer : Disclaimer {
override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html")
override suspend fun accept() {}
override suspend fun isAccepted(): Boolean = false
}
class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html")
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import org.rekotlin.Action
sealed class DisclaimerAction : Action {
data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction()
data class Show(
val from: DisclaimerSource,
val callback: DisclaimerCallback? = null,
) : DisclaimerAction()
object AcceptDisclaimer : DisclaimerAction()
object OnBackPressed : DisclaimerAction()
data class OnProgressStateChanged(val state: ProgressState?) : DisclaimerAction()
}

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.mainScope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
class DisclaimerMiddleware {
val disclaimerMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
state()?.let { handleDisclaimerMiddleware(action, it) }
next(action)
}
}
}
}
private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
val state = appState.disclaimerState
when (action) {
is DisclaimerAction.Show -> {
store.dispatchNavigationAction {
push(AppRoute.Disclaimer(isTosAccepted = action.from == DisclaimerSource.Details))
}
}
is DisclaimerAction.AcceptDisclaimer -> {
mainScope.launch {
state.disclaimer.accept()
store.dispatchNavigationAction(AppRouter::pop)
state.callback?.onAccept?.invoke()
}
}
is DisclaimerAction.OnBackPressed -> {
store.dispatchNavigationAction(AppRouter::pop)
state.callback?.onDismiss?.invoke()
}
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object DisclaimerReducer {
fun reduce(action: Action, state: AppState): DisclaimerState = internalReduce(action, state)
}
private fun internalReduce(action: Action, state: AppState): DisclaimerState {
if (action !is DisclaimerAction) return state.disclaimerState
val disclaimerState = state.disclaimerState
return when (action) {
is DisclaimerAction.SetDisclaimer -> disclaimerState.copy(
disclaimer = action.disclaimer,
)
is DisclaimerAction.Show -> disclaimerState.copy(
showedFrom = action.from,
callback = action.callback,
)
is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy(
callback = null,
progressState = null,
)
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
progressState = action.state,
)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.tap.features.disclaimer.redux
enum class DisclaimerSource {
Home, Details
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.DummyDisclaimer
import org.rekotlin.StateType
data class DisclaimerState(
val disclaimer: Disclaimer = DummyDisclaimer(),
val showedFrom: DisclaimerSource = DisclaimerSource.Home,
val callback: DisclaimerCallback? = null,
val progressState: ProgressState? = null,
) : StateType
data class DisclaimerCallback(
val onAccept: VoidCallback? = null,
val onDismiss: VoidCallback? = null,
)

View file

@ -1,139 +0,0 @@
package com.tangem.tap.features.disclaimer.ui
import android.os.Bundle
import android.view.View
import android.view.View.OVER_SCROLL_NEVER
import android.webkit.WebView
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDisclaimerBinding
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubscriber<DisclaimerState> {
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
private val webViewClient = DisclaimerWebViewClient()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(handler = this)
binding.apply {
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
webView.apply {
settings.allowFileAccess = false
settings.javaScriptEnabled = false
overScrollMode = OVER_SCROLL_NEVER
webViewClient = this@DisclaimerFragment.webViewClient
}
webView.hide()
groupError.hide()
groupAccept.hide()
groupLoading.hide()
btnAccept.setOnClickListener {
store.dispatch(DisclaimerAction.AcceptDisclaimer)
}
btnRepeat.setOnClickListener {
webViewClient.reset()
}
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_secondary)
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.disclaimerState == newState.disclaimerState }
.select(AppState::disclaimerState)
}
}
override fun onStop() {
webViewClient.onProgressStateChanged = null
store.unsubscribe(this)
super.onStop()
}
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (store.state.disclaimerState.showedFrom) {
DisclaimerSource.Home -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
DisclaimerSource.Details -> {
super.configureTransitions()
}
}
}
override fun handleOnBackPressed() {
store.dispatch(DisclaimerAction.OnBackPressed)
}
override fun newState(state: DisclaimerState) {
return with(binding) {
if (activity == null || view == null) return
updateUiVisibility(state.disclaimer, state.progressState)
}
}
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
when (progressState) {
ProgressState.Loading -> {
root.beginDelayedTransition()
webView.hide()
groupError.hide()
groupAccept.hide()
groupLoading.show()
}
ProgressState.Done -> {
root.beginDelayedTransition()
groupError.hide()
groupLoading.hide()
webView.show()
lifecycleScope.launch {
groupAccept.show(!disclaimer.isAccepted())
}
}
ProgressState.Error -> {
root.beginDelayedTransition()
webView.show()
groupAccept.show()
groupLoading.hide()
groupError.hide()
webView.loadLocalTermsOfServices()
}
else -> {
webView.setBackgroundColor(resources.getColor(R.color.transparent, null))
webView.loadUrl(disclaimer.getUri().toString())
}
}
}
private fun WebView.loadLocalTermsOfServices() {
loadData(localTermsOfServices, "text/html", "UTF-8")
}
}

View file

@ -1,64 +0,0 @@
package com.tangem.tap.features.disclaimer.ui
import android.graphics.Bitmap
import android.webkit.*
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.common.entities.ProgressState
class DisclaimerWebViewClient : WebViewClient() {
var onProgressStateChanged: ((ProgressState) -> Unit)? = null
private var loadingUrl: String? = null
private var loadedUrl: String? = null
private var progressState: ProgressState = ProgressState.Loading
set(value) {
field = value
onProgressStateChanged?.invoke(value)
}
fun reset() {
loadingUrl = null
loadedUrl = null
progressState = ProgressState.Loading
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (loadingUrl != url) progressState = ProgressState.Loading
loadingUrl = url
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (loadedUrl != url) progressState = ProgressState.Done
loadedUrl = url
}
override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) {
super.onReceivedError(view, resourceRequest, error)
error?.let { progressState = ProgressState.Error }
}
override fun onReceivedHttpError(
view: WebView?,
resourceRequest: WebResourceRequest?,
errorResponse: WebResourceResponse?,
) {
super.onReceivedHttpError(view, resourceRequest, errorResponse)
ifNotNull(resourceRequest, errorResponse) { request, response ->
if (request.url?.toString() != loadingUrl || response.statusCode < RESPONSE_USER_ERROR_STATUS_CODE) return
if (progressState != ProgressState.Done) return
progressState = ProgressState.Error
}
}
companion object {
private const val RESPONSE_USER_ERROR_STATUS_CODE = 400
}
}

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.disclaimer.ui
internal val localTermsOfServices = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Legal Disclaimer</title>
<style>
@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Light.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Light.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Light.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Light.ttf) format("truetype");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Regular.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Regular.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Regular.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Regular.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Medium.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Medium.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Medium.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Medium.ttf) format("truetype");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Semibold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Semibold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Bold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Bold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Bold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Bold.ttf) format("truetype");font-weight:700;font-style:normal;font-display:swap}*{margin:0;padding:0}body{padding:0;margin:1rem;color:rgb(0 0 0);font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}h1{font-weight:700;font-size:30px;line-height:1.2;margin-bottom:1rem}p{font-size:16px;line-height:20px;letter-spacing:-.24px;margin-bottom:1rem}
</style>
</head>
<body><h1>Legal Disclaimer</h1>
<p>&nbsp; &nbsp; 1. Tangem application (Software)</p>
<p>&nbsp; &nbsp; The Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The
Software DOES NOT:</p>
<p>&nbsp; &nbsp; a) Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain
wallets holding digital assets, including crypto-currency.</p>
<p>&nbsp; &nbsp; b) Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases
that can be used to restore or to copy private (secret) keys to blockchain wallets holding digital assets, including
crypto-currency.</p>
<p>&nbsp; &nbsp; c) Provide exchange, trading, investment services on behalf of Tangem AG.</p>
<p>&nbsp; &nbsp; 2. Risks related to the use of Software</p>
<p>&nbsp; &nbsp; Tangem will not be responsible for any losses, damages or claims arising from events falling within the
scope of the following five categories:</p>
<p>&nbsp; &nbsp; a) Mistakes made by the user of any cryptocurrency-related software or service, e.g., forgotten
passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Cards.</p>
<p>&nbsp; &nbsp; b) Problems of Software and/or any blockchain- or cryptocurrency- related software or service, e.g.,
corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.</p>
<p>&nbsp; &nbsp; c) Technical failures in the hardware of the user, including Cards, of any cryptocurrency-related
software or service, e.g., data loss due to a faulty or damaged storage device.</p>
<p>&nbsp; &nbsp; d) Security problems experienced by the user of any cryptocurrency-related software or service, e.g.,
unauthorized access to users&apos; wallets and/or accounts.</p>
<p>&nbsp; &nbsp; e) Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy
of service providers, information security attacks on service providers, and fraud conducted by third parties.</p>
<p>&nbsp; &nbsp; 3. Trading and Investment risks</p>
<p>&nbsp; &nbsp; There is considerable exposure to risk in any crypto-currency or other digital asset exchange
transaction. Any transaction involving currencies involves risks including, but not limited to, the potential for
changing economic conditions that may substantially affect the price or liquidity of a currency. Investments in
crypto-currency exchange speculation may also be susceptible to sharp rises and falls as the relevant market values
fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.</p>
<p>&nbsp; &nbsp; 4. Electronic Trading Risks</p>
<p>&nbsp; &nbsp; Before you engage in transactions using an electronic system, you should carefully review the rules and
regulations of the exchanges offering the system and/or listing the instruments you intend to trade. Online trading
has inherent risk due to system response and access times that may vary due to market conditions, system
performance, and other factors. You should understand these and additional risks before trading.</p>
<p>&nbsp; &nbsp; 5. Compliance with tax obligations &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; The users of the Software are solely responsible to determinate what, if any, taxes apply to their
crypto-currency transactions. The owners of, or contributors to, the Software are NOT responsible for determining
the taxes that apply to crypto-currency transactions. &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; 6. No warranties &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; The Software is provided on an &quot;as is&quot; basis without any warranties of any kind regarding the
Software and/or any content, data, materials and/or services provided on the Software. &nbsp; &nbsp; &nbsp;
&nbsp;</p>
<p>&nbsp; &nbsp; 7. Limitation of liability</p>
<p>&nbsp; &nbsp; Unless otherwise required by law, in no event shall the owners of, or contributors to, the Software be
liable for any damages of any kind, including, but not limited to, loss of use, loss of profits, or loss of data
arising out of or in any way connected with the use of the Software. In no way are the owners of, or contributors
to, the Software responsible for the actions, decisions, or other behavior taken or not taken by you in reliance
upon the Software.</p>
<p>&nbsp; &nbsp; 8. Last amendment</p>
<p>&nbsp; &nbsp; This disclaimer was amended for the last time on October 1st, 2020.</p></body>
</html>
""".trimIndent()

View file

@ -58,7 +58,9 @@ internal fun StoriesScreen(
}
StoriesScreenContent(
modifier = Modifier.fillMaxSize().testTag(TestTags.STORIES_SCREEN),
modifier = Modifier
.fillMaxSize()
.testTag(TestTags.STORIES_SCREEN),
config = StoriesScreenContentConfig(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,
@ -157,7 +159,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
duration = currentStoryDuration,
)
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
is Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
isPaused = isPaused,
stepDuration = currentStoryDuration,
)

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.home.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import javax.inject.Inject
class HomeFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isMigrateUserCountryCodeEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "MIGRATE_USER_COUNTRY_CODE_ENABLED")
}

View file

@ -15,6 +15,4 @@ sealed class HomeAction : Action {
data class ReadCard(val scope: CoroutineScope) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
}

View file

@ -10,16 +10,10 @@ object HomeReducer {
private fun internalReduce(action: Action, appState: AppState): HomeState {
if (action !is HomeAction) return appState.homeState
var state = appState.homeState
when (action) {
return when (action) {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
appState.homeState.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.UpdateCountryCode -> {
state.onCountryCodeUpdate(state, action.userCountryCode)
}
else -> {}
else -> appState.homeState
}
return state
}

View file

@ -1,56 +1,24 @@
package com.tangem.tap.features.home.redux
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.StateType
import java.util.Locale
@Immutable
data class HomeState(
val scanInProgress: Boolean = false,
val stories: List<Stories> = initDefaultStories(),
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),
) : StateType {
val firstStory: Stories
get() = stories[0]
val firstStory: Stories get() = stories[0]
fun stepOf(story: Stories): Int = stories.indexOf(story)
fun onCountryCodeUpdate(homeState: HomeState, countryCode: String) {
val isNewWalletAvailable = !(countryCode == RUSSIA_COUNTRY_CODE || countryCode == BELARUS_COUNTRY_CODE)
homeState.stories.forEach {
it.isNewWalletAvailable.value = isNewWalletAvailable
}
}
companion object {
private const val RUSSIA_COUNTRY_CODE = "ru"
private const val BELARUS_COUNTRY_CODE = "by"
fun initDefaultStories(): List<Stories> = listOf(
Stories.TangemIntro,
Stories.RevolutionaryWallet,
Stories.UltraSecureBackup,
Stories.Currencies,
Stories.Web3,
Stories.WalletForEveryone,
)
fun isNewWalletAvailableInit(): Boolean {
val locale = Locale.getDefault().language
return !(locale == RUSSIA_COUNTRY_CODE || locale == BELARUS_COUNTRY_CODE)
}
}
}
sealed class Stories(
val duration: Int,
val isNewWalletAvailable: MutableState<Boolean> = mutableStateOf(HomeState.isNewWalletAvailableInit()),
) {
object TangemIntro : Stories(duration = 6000)
object RevolutionaryWallet : Stories(duration = 6000)
object UltraSecureBackup : Stories(duration = 6000)
object Currencies : Stories(duration = 6000)
object Web3 : Stories(duration = 6000)
object WalletForEveryone : Stories(duration = 6000)
enum class Stories(val duration: Int = 6000) {
TangemIntro,
RevolutionaryWallet,
UltraSecureBackup,
Currencies,
Web3,
WalletForEveryone,
}

View file

@ -16,11 +16,13 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -47,6 +49,8 @@ internal class MainViewModel @Inject constructor(
stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
private val homeFeatureToggles: HomeFeatureToggles,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -67,6 +71,14 @@ internal class MainViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
viewModelScope.launch {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
}
}
updateAppCurrencies()
observeFlips()
displayBalancesHidingStatusToast()

View file

@ -1,22 +1,31 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -138,6 +147,39 @@ object OnboardingHelper {
}
}
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) {
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
} else {
globalState.userCountryCode == RUSSIA_COUNTRY_CODE
}
if (isRussia) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
}
}
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,

View file

@ -4,12 +4,8 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
@ -20,7 +16,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
@ -178,23 +173,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
return
}
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
blockchain,
null,
scanResponse.derivationStyleProvider,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = scanResponse,
globalState = globalState,
)
}
is OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)

View file

@ -6,16 +6,12 @@ import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
@ -26,7 +22,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
@ -301,24 +296,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
return
}
val scanResponse = onboardingManager?.scanResponse ?: return
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
blockchain,
null,
scanResponse.derivationStyleProvider,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = onboardingManager?.scanResponse ?: return,
globalState = globalState,
)
}
TwinCardsAction.Done -> {
val scanResponse = getScanResponse()

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
@ -70,16 +71,26 @@ object TradeCryptoMiddleware {
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = topUrl?.let {
AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it)
}
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(data = dialogData))
return
}
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
scope.launch {
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
} else {
state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE
}
if (action.checkUserLocation && isRussia) {
val dialogData = topUrl?.let {
AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it)
}
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(data = dialogData))
return@launch
}
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
@ -97,13 +108,13 @@ object TradeCryptoMiddleware {
walletManager = walletManager,
destinationAddress = currency.contractAddress,
)
return@launch
}
return
}
topUrl?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
topUrl?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -29,8 +30,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
@ -41,6 +40,7 @@ import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.StateType
@ -80,11 +80,11 @@ data class DaggerGraphState(
val getFeedbackEmailUseCase: GetFeedbackEmailUseCase? = null,
val getCardInfoUseCase: GetCardInfoUseCase? = null,
val assetLoader: AssetLoader? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = null,
val stakingRouter: StakingRouter? = null,
val urlOpener: UrlOpener? = null,
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles? = null,
val pushNotificationsRouter: PushNotificationsRouter? = null,
val homeFeatureToggles: HomeFeatureToggles? = null,
val getUserCountryUseCase: GetUserCountryUseCase? = null,
) : StateType

View file

@ -6,14 +6,12 @@ import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.ManageTokensToggles
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
@ -25,11 +23,9 @@ import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragmen
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
@ -59,8 +55,6 @@ internal class ChildFactory @Inject constructor(
private val qrScanningRouter: QrScanningRouter,
private val stakingRouter: StakingRouter,
private val testerRouter: TesterRouter,
private val detailsFeatureToggles: DetailsFeatureToggles,
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val manageTokensToggles: ManageTokensToggles,
private val pushNotificationRouter: PushNotificationsRouter,
) {
@ -98,29 +92,21 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { CardSettingsFragment() })
}
is AppRoute.Details -> {
if (detailsFeatureToggles.isRedesignEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DetailsComponent.Params(route.userWalletId),
componentFactory = detailsComponentFactory,
)
} else {
route.asFragmentChild(Provider { DetailsFragment() })
}
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DetailsComponent.Params(route.userWalletId),
componentFactory = detailsComponentFactory,
)
}
is AppRoute.DetailsSecurity -> {
route.asFragmentChild(Provider { SecurityModeFragment() })
}
is AppRoute.Disclaimer -> {
if (pushNotificationsFeatureToggles.isPushNotificationsEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DisclaimerComponent.Params(route.isTosAccepted),
componentFactory = disclaimerComponentFactory,
)
} else {
route.asFragmentChild(Provider { DisclaimerFragment() })
}
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DisclaimerComponent.Params(route.isTosAccepted),
componentFactory = disclaimerComponentFactory,
)
}
is AppRoute.Home -> {
route.asFragmentChild(Provider { HomeFragment() })

View file

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_secondary"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:navigationIconTint="@color/icon_primary_1"
app:titleTextColor="@color/text_primary_1"
app:title="@string/disclaimer_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_secondary"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="74dp"
app:layout_constraintBottom_toTopOf="@id/btn_accept"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/half_transparent_overlay"
android:layout_width="match_parent"
android:layout_height="110dp"
android:background="@drawable/bg_half_transparent_overlay"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:lineSpacingExtra="5sp"
android:text="@string/disclaimer_error_loading"
android:textAlignment="center"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/btn_repeat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="@+id/btn_repeat"
style="@style/TapSecondaryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/common_retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_error" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_accept"
style="@style/TapPrimaryButton"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:text="@string/common_accept"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="btn_repeat, tv_error" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="progress" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="btn_accept, half_transparent_overlay" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -227,13 +227,16 @@ sealed class AppRoute(val path: String) : Route {
data class Swap(
val currency: CryptoCurrency,
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}"), RouteBundleParams {
val isInitialReverseOrder: Boolean = false,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GeoResponse(
@Json(name = "code") val code: String,
)

View file

@ -7,10 +7,6 @@
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.14.0"
},
{
"name": "GENERATE_XPUB_ENABLED",
"version": "5.12.0"
},
{
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "undefined"
@ -19,14 +15,6 @@
"name": "STAKING_ENABLED",
"version": "5.15.0"
},
{
"name": "DETAILS_REDESIGN_ENABLED",
"version": "5.13.0"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "5.13.0"
},
{
"name": "MARKETS_ENABLED",
"version": "5.15.0"
@ -38,5 +26,9 @@
{
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.16.0"
},
{
"name": "MIGRATE_USER_COUNTRY_CODE_ENABLED",
"version": "5.16.0"
}
]

View file

@ -24,6 +24,7 @@ internal class DevFeatureTogglesManager(
) : MutableFeatureTogglesManager {
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localFeatureTogglesStorage.init()
@ -32,8 +33,12 @@ internal class DevFeatureTogglesManager(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
featureTogglesMap = localFeatureTogglesStorage.featureToggles
val localFeatureToggles = localFeatureTogglesStorage.featureToggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
localFeatureTogglesMap = localFeatureToggles
featureTogglesMap = localFeatureToggles
.mapValues { resultToggle ->
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
}
@ -42,16 +47,27 @@ internal class DevFeatureTogglesManager(
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap)
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap)
}
override suspend fun recoverLocalConfig() {
featureTogglesMap = localFeatureTogglesMap.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
featureTogglesMap = map
}
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
}
}

View file

@ -7,9 +7,15 @@ package com.tangem.core.featuretoggle.manager
*/
interface MutableFeatureTogglesManager : FeatureTogglesManager {
/** Check if the current state of the feature toggles matches the local config state. */
fun isMatchLocalConfig(): Boolean
/** Get feature toggles */
fun getFeatureToggles(): Map<String, Boolean>
/** Change availability [isEnabled] of toggle with name [name] */
suspend fun changeToggle(name: String, isEnabled: Boolean)
/** Recover local config state */
suspend fun recoverLocalConfig()
}

View file

@ -75,6 +75,8 @@ fun getActiveIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
}
}
@ -151,6 +153,8 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
}
}
@ -224,6 +228,8 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
}
}
@ -300,6 +306,8 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22
else -> R.drawable.ic_alert_24
}
}
@ -376,6 +384,8 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M10.426,16.82C9.756,17.299 8.955,17.56 8.131,17.568C6.707,17.566 5.405,16.793 4.418,15.369C3.531,14.007 3.04,12.425 3,10.801C3,10.77 3.01,10.74 3.028,10.715C3.046,10.691 3.071,10.672 3.1,10.662L4.071,10.229C4.089,10.22 4.108,10.215 4.128,10.215C4.149,10.214 4.169,10.218 4.187,10.225C4.206,10.233 4.223,10.244 4.237,10.258C4.251,10.273 4.262,10.29 4.269,10.308C4.269,10.328 4.288,10.347 4.288,10.367C4.286,11.982 4.718,13.51 5.489,14.639C6.217,15.708 7.167,16.283 8.156,16.284C8.691,16.286 9.214,16.128 9.659,15.831C9.691,15.814 9.729,15.809 9.764,15.817C9.8,15.824 9.833,15.844 9.856,15.872C10.053,16.129 10.251,16.366 10.467,16.603C10.48,16.639 10.483,16.679 10.476,16.717C10.469,16.755 10.452,16.791 10.426,16.82ZM17.567,15.389C16.596,16.791 15.269,17.579 13.847,17.577C12.425,17.575 11.12,16.783 10.135,15.378C9.208,14.033 8.697,12.252 8.7,10.369C8.702,8.631 9.713,7.267 10.978,7.269C12.243,7.271 13.251,8.638 13.248,10.376C13.268,11.891 12.905,13.386 12.193,14.724C12.184,14.741 12.172,14.756 12.157,14.769C12.143,14.781 12.125,14.791 12.107,14.796C12.088,14.802 12.069,14.804 12.05,14.801C12.031,14.799 12.012,14.793 11.995,14.783C11.976,14.783 11.976,14.762 11.955,14.743C11.837,14.604 11.718,14.466 11.62,14.327C11.501,14.156 11.394,13.978 11.297,13.793C11.288,13.772 11.283,13.748 11.283,13.724C11.283,13.7 11.288,13.677 11.297,13.655C11.748,12.613 11.979,11.489 11.976,10.354C11.978,9.505 11.543,8.535 10.969,8.534C10.396,8.533 9.96,9.502 9.958,10.351C9.956,11.966 10.388,13.495 11.159,14.623C11.889,15.691 12.837,16.265 13.825,16.267C14.814,16.268 15.765,15.677 16.497,14.632C17.27,13.502 17.708,11.985 17.711,10.363C17.713,8.742 17.282,7.228 16.51,6.096C16.29,5.763 16.024,5.463 15.72,5.205C15.694,5.175 15.678,5.139 15.674,5.1C15.671,5.061 15.68,5.021 15.701,4.988C15.72,4.967 15.741,4.948 15.761,4.948L16.731,4.515C16.758,4.5 16.789,4.496 16.819,4.503C16.849,4.511 16.875,4.529 16.892,4.554C17.139,4.804 17.364,5.075 17.565,5.363C18.492,6.708 19.003,8.488 19,10.372C18.997,12.255 18.498,14.047 17.567,15.389Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#A466FF"/>
<path
android:pathData="M10.426,16.82C9.756,17.299 8.955,17.56 8.131,17.568C6.707,17.566 5.405,16.793 4.418,15.369C3.531,14.007 3.04,12.425 3,10.801C3,10.77 3.01,10.74 3.028,10.715C3.046,10.691 3.071,10.672 3.1,10.662L4.071,10.229C4.089,10.22 4.108,10.215 4.128,10.215C4.149,10.214 4.169,10.218 4.187,10.225C4.206,10.233 4.223,10.244 4.237,10.258C4.251,10.273 4.262,10.29 4.269,10.308C4.269,10.328 4.288,10.347 4.288,10.367C4.286,11.982 4.718,13.51 5.489,14.639C6.217,15.708 7.167,16.283 8.156,16.284C8.691,16.286 9.214,16.128 9.659,15.831C9.691,15.814 9.729,15.809 9.764,15.817C9.8,15.824 9.833,15.844 9.856,15.872C10.053,16.129 10.251,16.366 10.467,16.603C10.48,16.639 10.483,16.679 10.476,16.717C10.469,16.755 10.452,16.791 10.426,16.82ZM17.567,15.389C16.596,16.791 15.269,17.579 13.847,17.577C12.425,17.575 11.12,16.783 10.135,15.378C9.208,14.033 8.697,12.252 8.7,10.369C8.702,8.631 9.713,7.267 10.978,7.269C12.243,7.271 13.251,8.638 13.248,10.376C13.268,11.891 12.905,13.386 12.193,14.724C12.184,14.741 12.172,14.756 12.157,14.769C12.143,14.781 12.125,14.791 12.107,14.796C12.088,14.802 12.069,14.804 12.05,14.801C12.031,14.799 12.012,14.793 11.995,14.783C11.976,14.783 11.976,14.762 11.955,14.743C11.837,14.604 11.718,14.466 11.62,14.327C11.501,14.156 11.394,13.978 11.297,13.793C11.288,13.772 11.283,13.748 11.283,13.724C11.283,13.7 11.288,13.677 11.297,13.655C11.748,12.613 11.979,11.489 11.976,10.354C11.978,9.505 11.543,8.535 10.969,8.534C10.396,8.533 9.96,9.502 9.958,10.351C9.956,11.966 10.388,13.495 11.159,14.623C11.889,15.691 12.837,16.265 13.825,16.267C14.814,16.268 15.765,15.677 16.497,14.632C17.27,13.502 17.708,11.985 17.711,10.363C17.713,8.742 17.282,7.228 16.51,6.096C16.29,5.763 16.024,5.463 15.72,5.205C15.694,5.175 15.678,5.139 15.674,5.1C15.671,5.061 15.68,5.021 15.701,4.988C15.72,4.967 15.741,4.948 15.761,4.948L16.731,4.515C16.758,4.5 16.789,4.496 16.819,4.503C16.849,4.511 16.875,4.529 16.892,4.554C17.139,4.804 17.364,5.075 17.565,5.363C18.492,6.708 19.003,8.488 19,10.372C18.997,12.255 18.498,14.047 17.567,15.389Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -30,5 +30,6 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
// endregion
}

View file

@ -1,17 +1,31 @@
package com.tangem.data.settings
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.GeoResponse
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Locale
internal class DefaultSettingsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val appLogsStore: AppLogsStore,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : SettingsRepository {
private val userCountryFlow = MutableStateFlow<UserCountry?>(value = null)
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
@ -99,4 +113,38 @@ internal class DefaultSettingsRepository(
override suspend fun setMarketsTooltipShown(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = !value)
}
override suspend fun getUserCountryCodeSync(): UserCountry? {
// If user country code is already set, return it
val countryCode = userCountryFlow.value
if (countryCode != null) return countryCode
coroutineScope {
launch { fetchUserCountryCode() }
}
return null
}
override suspend fun fetchUserCountryCode() {
Timber.i("Start fetching user country code")
withContext(dispatchers.io) {
val country = runCatching { tangemTechApi.getUserCountryCode() }
.fold(
onSuccess = GeoResponse::code,
onFailure = { Locale.getDefault().country },
)
.lowercase()
val code = when (country) {
UserCountry.Russia.code -> UserCountry.Russia
else -> UserCountry.Other(code = country)
}
Timber.i("User code country is $code")
userCountryFlow.value = code
}
}
}

View file

@ -4,12 +4,14 @@ import com.tangem.data.settings.DefaultAppRatingRepository
import com.tangem.data.settings.DefaultPermissionRepository
import com.tangem.data.settings.DefaultPromoSettingsRepository
import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,8 +27,15 @@ internal object SettingsDataModule {
fun provideSettingsRepository(
appPreferencesStore: AppPreferencesStore,
appLogsStore: AppLogsStore,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): SettingsRepository {
return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore, appLogsStore = appLogsStore)
return DefaultSettingsRepository(
appPreferencesStore = appPreferencesStore,
appLogsStore = appLogsStore,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -1,5 +1,7 @@
package com.tangem.domain.settings.repositories
import com.tangem.domain.settings.usercountry.models.UserCountry
interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean
@ -33,4 +35,8 @@ interface SettingsRepository {
suspend fun shouldShowMarketsTooltip(): Boolean
suspend fun setMarketsTooltipShown(value: Boolean)
suspend fun getUserCountryCodeSync(): UserCountry?
suspend fun fetchUserCountryCode()
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.settings.usercountry
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.UserCountryError
/**
* Fetch user country use case
*
* @property settingsRepository settings repository
*
[REDACTED_AUTHOR]
*/
class FetchUserCountryUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<UserCountryError, Unit> {
return either {
catch(
block = { settingsRepository.fetchUserCountryCode() },
catch = { raise(UserCountryError.DataError) },
)
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.domain.settings.usercountry
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
/**
* Get user country code use case
*
* @property settingsRepository settings repository
*
[REDACTED_AUTHOR]
*/
class GetUserCountryUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<UserCountryError, UserCountry> {
return either {
val userCountryCode = catch(
block = { settingsRepository.getUserCountryCodeSync() },
catch = { raise(UserCountryError.DataError) },
)
ensureNotNull(userCountryCode) { UserCountryError.NotSetup }
userCountryCode
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings.usercountry.models
/**
* User country
*
[REDACTED_AUTHOR]
*/
sealed class UserCountry(open val code: String) {
data object Russia : UserCountry("ru")
data class Other(override val code: String) : UserCountry(code)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.settings.usercountry.models
/**
* User country error
*
[REDACTED_AUTHOR]
*/
sealed interface UserCountryError {
/** User country is still loading */
data object NotSetup : UserCountryError
/** Data error */
data object DataError : UserCountryError
}

View file

@ -1,6 +0,0 @@
package com.tangem.features.details
interface DetailsFeatureToggles {
val isRedesignEnabled: Boolean
}

View file

@ -1,11 +0,0 @@
package com.tangem.features.details
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : DetailsFeatureToggles {
override val isRedesignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("DETAILS_REDESIGN_ENABLED")
}

View file

@ -1,21 +0,0 @@
package com.tangem.features.details.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.details.DefaultDetailsFeatureToggles
import com.tangem.features.details.DetailsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureModule {
@Provides
@Singleton
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
return DefaultDetailsFeatureToggles(featureTogglesManager)
}
}

View file

@ -151,6 +151,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
AppRoute.Swap(
currency = cryptoCurrencyData.status.currency,
userWalletId = cryptoCurrencyData.userWallet.walletId,
isInitialReverseOrder = true,
),
)
}

View file

@ -1,9 +0,0 @@
package com.tangem.features.pushnotifications.api.featuretoggles
/**
* Push notifications feature toggles
*/
interface PushNotificationsFeatureToggles {
/** Availability of push notifications */
val isPushNotificationsEnabled: Boolean
}

View file

@ -1,24 +0,0 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* DI module provides implementation of [PushNotificationsFeatureToggles]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object PushNotificationsFeatureTogglesModule {
@Provides
@Singleton
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles {
return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.features.pushnotifications.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
internal class DefaultPushNotificationsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : PushNotificationsFeatureToggles {
override val isPushNotificationsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
}

View file

@ -18,6 +18,14 @@ data class TokensDataStateExpress(
}
}
fun TokensDataStateExpress.getGroupWithReverse(isReverseFromTo: Boolean): CurrenciesGroup {
return if (isReverseFromTo) {
this.fromGroup
} else {
this.toGroup
}
}
data class CurrenciesGroup(
val available: List<CryptoCurrencySwapInfo>,
val unavailable: List<CryptoCurrencySwapInfo>,

View file

@ -2,31 +2,34 @@ package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse
import java.math.BigDecimal
internal class DefaultInitialToCurrencyResolver(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val swapTransactionRepository: SwapTransactionRepository,
) : InitialToCurrencyResolver {
override suspend fun tryGetFromCache(
userWallet: UserWallet,
initialCryptoCurrency: CryptoCurrency,
state: TokensDataStateExpress,
isReverseFromTo: Boolean,
): CryptoCurrencyStatus? {
val selectedId = getSelectedWalletSyncUseCase().getOrNull() ?: return null
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(selectedId.walletId) ?: return null
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null
return if (id != initialCryptoCurrency.id.value) {
state.toGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
val group = state.getGroupWithReverse(isReverseFromTo)
group.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
} else {
null
}
}
override fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus? {
return state.toGroup.available.maxByOrNull {
override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? {
val group = state.getGroupWithReverse(isReverseFromTo)
return group.available.maxByOrNull {
it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO
}?.currencyStatus
}

View file

@ -2,14 +2,17 @@ package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
interface InitialToCurrencyResolver {
suspend fun tryGetFromCache(
userWallet: UserWallet,
initialCryptoCurrency: CryptoCurrency,
state: TokensDataStateExpress,
isReverseFromTo: Boolean,
): CryptoCurrencyStatus?
fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus?
fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus?
}

View file

@ -79,9 +79,10 @@ interface SwapInteractor {
*/
fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount
suspend fun selectInitialCurrencyToSwap(
suspend fun getInitialCurrencyToSwap(
initialCryptoCurrency: CryptoCurrency,
state: TokensDataStateExpress,
isReverseFromTo: Boolean,
): CryptoCurrencyStatus?
fun getNativeToken(networkId: String): CryptoCurrency

View file

@ -965,13 +965,15 @@ internal class SwapInteractorImpl @AssistedInject constructor(
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals)
}
override suspend fun selectInitialCurrencyToSwap(
override suspend fun getInitialCurrencyToSwap(
initialCryptoCurrency: CryptoCurrency,
state: TokensDataStateExpress,
isReverseFromTo: Boolean,
): CryptoCurrencyStatus? {
return initialToCurrencyResolver.tryGetFromCache(initialCryptoCurrency, state)
?: initialToCurrencyResolver.tryGetWithMaxAmount(state)
?: state.toGroup.available.firstOrNull()?.currencyStatus
val group = state.getGroupWithReverse(isReverseFromTo)
return initialToCurrencyResolver.tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo)
?: initialToCurrencyResolver.tryGetWithMaxAmount(state, isReverseFromTo)
?: group.available.firstOrNull()?.currencyStatus
}
override fun getNativeToken(networkId: String): CryptoCurrency {

View file

@ -5,8 +5,6 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.*
import com.tangem.lib.crypto.TransactionManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -60,11 +58,9 @@ internal class SwapDomainModule {
@Provides
@Singleton
fun provideInitialToCurrencyResolver(
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
swapTransactionRepository: SwapTransactionRepository,
): InitialToCurrencyResolver {
return DefaultInitialToCurrencyResolver(
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
swapTransactionRepository = swapTransactionRepository,
)
}

View file

@ -84,6 +84,9 @@ internal class SwapViewModel @Inject constructor(
?.unbundle(UserWalletId.serializer())
?: error("no expected parameter UserWalletId found")
private val isInitiallyReversed: Boolean = savedStateHandle.get<Boolean>(AppRoute.Swap.IS_INITIAL_REVERSE_ORDER)
?: false
private val swapInteractor = swapInteractorFactory.create(userWalletId)
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
@ -137,7 +140,7 @@ internal class SwapViewModel @Inject constructor(
uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back)
} else {
initialCryptoCurrencyStatus = cryptoCurrencyStatus
initTokens()
initTokens(isInitiallyReversed)
}
}
}
@ -180,18 +183,21 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens))
}
private fun initTokens() {
private fun initTokens(isReverseFromTo: Boolean) {
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.getTokensDataState(initialCryptoCurrency)
}.onSuccess { state ->
updateTokensState(state)
val selectedCurrency = swapInteractor.getInitialCurrencyToSwap(
initialCryptoCurrency = initialCryptoCurrency,
state = state,
isReverseFromTo = isReverseFromTo,
)
applyInitialTokenChoice(
state,
swapInteractor.selectInitialCurrencyToSwap(
initialCryptoCurrency,
state,
),
state = state,
selectedCurrency = selectedCurrency,
isReverseFromTo = isReverseFromTo,
)
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
@ -215,6 +221,7 @@ internal class SwapViewModel @Inject constructor(
applyInitialTokenChoice(
state = TokensDataStateExpress.EMPTY,
selectedCurrency = null,
isReverseFromTo = isReverseFromTo,
)
uiState = stateBuilder.createInitialErrorState(
@ -227,33 +234,43 @@ internal class SwapViewModel @Inject constructor(
initialCryptoCurrency.network.backendId,
),
)
initTokens()
initTokens(isReverseFromTo)
}
}
}
}
private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) {
val fromCurrencyStatus = initialCryptoCurrencyStatus
dataState = dataState.copy(
fromCryptoCurrency = fromCurrencyStatus,
toCryptoCurrency = selectedCurrency,
tokensDataState = state,
)
private fun applyInitialTokenChoice(
state: TokensDataStateExpress,
selectedCurrency: CryptoCurrencyStatus?,
isReverseFromTo: Boolean,
) {
// exceptional case
if (selectedCurrency == null) {
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap)
uiState = stateBuilder.createNoAvailableTokensToSwapState(
uiStateHolder = uiState,
fromToken = fromCurrencyStatus,
)
} else {
startLoadingQuotes(
fromToken = fromCurrencyStatus,
toToken = selectedCurrency,
amount = lastAmount.value,
toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency),
fromToken = initialCryptoCurrencyStatus,
)
return
}
isOrderReversed = isReverseFromTo
val (fromCurrencyStatus, toCurrencyStatus) = if (isOrderReversed) {
selectedCurrency to initialCryptoCurrencyStatus
} else {
initialCryptoCurrencyStatus to selectedCurrency
}
dataState = dataState.copy(
fromCryptoCurrency = fromCurrencyStatus,
toCryptoCurrency = toCurrencyStatus,
tokensDataState = state,
)
startLoadingQuotes(
fromToken = fromCurrencyStatus,
toToken = toCurrencyStatus,
amount = lastAmount.value,
toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus),
)
}
private fun updateTokensState(tokenDataState: TokensDataStateExpress) {

View file

@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
@ -65,7 +68,6 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EnvironmentButtons(
uiModel: EnvironmentTogglesScreenUM.ApiInfoUM,

View file

@ -6,14 +6,37 @@ import kotlinx.collections.immutable.ImmutableList
/**
* Content state of feature toggles screen
*
* @property topBarState top bar state
* @property appVersion app version
* @property featureToggles feature toggles list
* @property onBackClick the lambda to be invoked when back button is pressed
* @property onToggleValueChange the lambda to be invoked when switch button is pressed
* @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed
* @property onRestartAppClick the lambda to be invoked when restart app button is pressed
*/
internal data class FeatureTogglesContentState(
val topBarState: TopBarState,
val appVersion: String,
val featureToggles: ImmutableList<TesterFeatureToggle>,
val onToggleValueChange: (String, Boolean) -> Unit,
val onBackClick: () -> Unit,
val onApplyChangesClick: () -> Unit,
)
val onToggleValueChange: (String, Boolean) -> Unit,
val onRestartAppClick: () -> Unit,
) {
/** Top bar state */
sealed interface TopBarState {
/**
* Config setup
*
* @property onBackClick the lambda to be invoked when back button is pressed
*/
data object ConfigSetup : TopBarState
/**
* Custom setup
*
* @property onRecoverClick the lambda to be invoked when recover button is pressed
*/
data class CustomSetup(val onRecoverClick: () -> Unit) : TopBarState
}
}

View file

@ -6,17 +6,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tester.impl.R
@ -37,22 +41,35 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
) {
stickyHeader {
AppBarWithBackButton(
onBackClick = state.onBackClick,
text = stringResource(id = R.string.feature_toggles),
)
stickyHeader { TopAppBar(state = state.topBarState, onBackClick = state.onBackClick) }
if (state.topBarState is FeatureTogglesContentState.TopBarState.CustomSetup) {
item(key = "warning_notification", contentType = "warning_notification") {
WarningNotification(
appVersion = state.appVersion,
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp),
)
}
}
items(state.featureToggles) { featureToggle ->
items(
items = state.featureToggles,
key = TesterFeatureToggle::name,
contentType = { "feature_toggle_item" },
) { featureToggle ->
FeatureToggleItem(
toggle = featureToggle,
onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) },
)
}
item {
PrimaryButton(
text = stringResource(id = R.string.apply_changes),
onClick = state.onApplyChangesClick,
text = stringResource(id = R.string.restart_app),
onClick = state.onRestartAppClick,
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
@ -61,12 +78,48 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
}
}
@Composable
private fun TopAppBar(state: FeatureTogglesContentState.TopBarState, onBackClick: () -> Unit) {
AppBarWithBackButtonAndIcon(
onBackClick = onBackClick,
text = stringResource(id = R.string.feature_toggles),
iconRes = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) {
R.drawable.ic_refresh_24
} else {
null
},
onIconClick = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) {
state.onRecoverClick
} else {
null
},
)
}
@Composable
private fun WarningNotification(appVersion: String, modifier: Modifier = Modifier) {
Notification(
config = NotificationConfig(
subtitle = resourceReference(
id = R.string.feature_toggles_custom_setup_warning_description,
wrappedList(appVersion),
),
iconResId = R.drawable.ic_alert_triangle_20,
title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title),
),
modifier = modifier,
)
}
@Composable
private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boolean) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing18),
.padding(
horizontal = TangemTheme.dimens.spacing18,
vertical = TangemTheme.dimens.spacing8,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -78,14 +131,8 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
maxLines = 1,
style = TangemTheme.typography.body2,
)
Switch(
checked = toggle.isEnabled,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = TangemTheme.colors.control.checked,
uncheckedThumbColor = TangemTheme.colors.control.unchecked,
),
)
TangemSwitch(onCheckedChange = onCheckedChange, checked = toggle.isEnabled)
}
}
@ -94,15 +141,23 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
@Composable
private fun PreviewFeatureTogglesScreen() {
TangemThemePreview {
var isCustomSetup by remember { mutableStateOf(value = true) }
FeatureTogglesScreen(
state = FeatureTogglesContentState(
topBarState = if (isCustomSetup) {
FeatureTogglesContentState.TopBarState.CustomSetup(onRecoverClick = { isCustomSetup = false })
} else {
FeatureTogglesContentState.TopBarState.ConfigSetup
},
appVersion = "5.15",
featureToggles = persistentListOf(
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
),
onToggleValueChange = { _, _ -> },
onBackClick = {},
onApplyChangesClick = {},
onToggleValueChange = { _, _ -> isCustomSetup = true },
onRestartAppClick = {},
),
)
}

View file

@ -11,10 +11,11 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -22,14 +23,14 @@ import javax.inject.Inject
* ViewModel for screen with list of feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
* @property dispatchers coroutine dispatchers provider
* @property appVersionProvider app version provider
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class FeatureTogglesViewModel @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
private val dispatchers: CoroutineDispatcherProvider,
private val appVersionProvider: AppVersionProvider,
) : ViewModel() {
/** Current ui state */
@ -41,28 +42,54 @@ internal class FeatureTogglesViewModel @Inject constructor(
"Feature toggle manager must be mutable (debug build type)"
}
/** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */
/** Setup navigation state property by router [router] and provides app restart method by [appFinisher] */
fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) {
uiState = uiState.copy(
onBackClick = router::back,
onApplyChangesClick = appFinisher::restart,
onRestartAppClick = appFinisher::restart,
)
}
private fun initState(): FeatureTogglesContentState {
return FeatureTogglesContentState(
topBarState = getConfigSetupState(),
appVersion = appVersionProvider.versionName,
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
onToggleValueChange = ::onToggleValueChange,
onBackClick = {},
onApplyChangesClick = {},
onRestartAppClick = {},
)
}
private fun onToggleValueChange(name: String, isEnabled: Boolean) {
viewModelScope.launch(dispatchers.main) {
viewModelScope.launch {
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
// delay for smoothly update animations
delay(timeMillis = 300)
uiState = uiState.copy(topBarState = getConfigSetupState())
}
}
private fun getConfigSetupState(): FeatureTogglesContentState.TopBarState {
return if (mutableFeatureTogglesManager.isMatchLocalConfig()) {
FeatureTogglesContentState.TopBarState.ConfigSetup
} else {
FeatureTogglesContentState.TopBarState.CustomSetup(
onRecoverClick = {
viewModelScope.launch {
mutableFeatureTogglesManager.recoverLocalConfig()
uiState = uiState.copy(
topBarState = getConfigSetupState(),
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
)
}
},
)
}
}

View file

@ -2,7 +2,9 @@
<resources>
<string name="tester_menu" translatable="false">Tester menu</string>
<string name="feature_toggles" translatable="false">Feature toggles</string>
<string name="apply_changes" translatable="false">Apply changes</string>
<string name="feature_toggles_custom_setup_warning_title" translatable="false">Custom setup</string>
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Feature toggles differs from v%s config</string>
<string name="restart_app" translatable="false">Restart app</string>
<string name="environment_toggles" translatable="false">Environment toggles</string>
<string name="tester_actions" translatable="false">Tester actions</string>
<string name="hide_all_currencies" translatable="false">Hide all currencies</string>

View file

@ -1,6 +0,0 @@
package com.tangem.features.tokendetails.featuretoggles
interface TokenDetailsFeatureToggles {
fun isGenerateXPubEnabled(): Boolean
}

View file

@ -1,21 +0,0 @@
package com.tangem.feature.tokendetails.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.feature.tokendetails.featuretoggles.DefaultTokenDetailsFeatureToggles
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TokenDetailsFeatureTogglesModule {
@Provides
@Singleton
fun provideTokenDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokenDetailsFeatureToggles {
return DefaultTokenDetailsFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.feature.tokendetails.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
internal class DefaultTokenDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED")
}

View file

@ -17,7 +17,6 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.converter.Converter
@ -28,7 +27,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsSkeletonStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletId: UserWalletId,
@ -98,12 +96,14 @@ internal class TokenDetailsSkeletonStateConverter(
cryptoCurrency: CryptoCurrency,
) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return
val isGenerateXPubEnabled = featureToggles.isGenerateXPubEnabled()
val isBitcoin = isBitcoin(cryptoCurrency.network.id.value)
val hasDerivations =
networkHasDerivationUseCase(userWallet.scanResponse, cryptoCurrency.network).getOrElse { false }
if (isGenerateXPubEnabled && isBitcoin && hasDerivations) {
val isBitcoin = isBitcoin(cryptoCurrency.network.id.value)
val hasDerivations = networkHasDerivationUseCase(
scanResponse = userWallet.scanResponse,
network = cryptoCurrency.network,
).getOrElse { false }
if (isBitcoin && hasDerivations) {
add(
TokenDetailsAppBarMenuConfig.MenuItem(
title = resourceReference(R.string.token_details_generate_xpub),

View file

@ -39,7 +39,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toImmutableList
@ -53,7 +52,6 @@ internal class TokenDetailsStateFactory(
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletId: UserWalletId,
@ -65,7 +63,6 @@ internal class TokenDetailsStateFactory(
private val skeletonStateConverter by lazy {
TokenDetailsSkeletonStateConverter(
clickIntents = clickIntents,
featureToggles = featureToggles,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
@ -390,10 +387,9 @@ internal class TokenDetailsStateFactory(
isSupported: Boolean,
): TokenDetailsAppBarMenuConfig? {
if (cardTypesResolver.isSingleWalletWithToken()) return null
val showGenerateExtendedKey = featureToggles.isGenerateXPubEnabled() && isSupported
return copy(
items = buildList {
if (showGenerateExtendedKey && hasDerivations) {
if (isSupported && hasDerivations) {
TokenDetailsAppBarMenuConfig.MenuItem(
title = resourceReference(R.string.token_details_generate_xpub),
textColorProvider = { TangemTheme.colors.text.primary1 },

View file

@ -71,7 +71,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -124,7 +123,6 @@ internal class TokenDetailsViewModel @Inject constructor(
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
getUserWalletUseCase: GetUserWalletUseCase,
tokenDetailsFeatureToggles: TokenDetailsFeatureToggles,
deepLinksRegistry: DeepLinksRegistry,
savedStateHandle: SavedStateHandle,
private val appRouter: AppRouter,
@ -159,13 +157,12 @@ internal class TokenDetailsViewModel @Inject constructor(
stakingEntryInfoProvider = Provider { stakingEntryInfo },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
clickIntents = this,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = tokenDetailsFeatureToggles,
stakingFeatureToggles = stakingFeatureToggles,
userWalletId = userWalletId,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
stakingFeatureToggles = stakingFeatureToggles,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {

View file

@ -28,7 +28,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.markets.MarketsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.Provider
@ -67,7 +66,6 @@ internal class WalletViewModel @Inject constructor(
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val marketsFeatureToggles: MarketsFeatureToggles,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -166,10 +164,9 @@ internal class WalletViewModel @Inject constructor(
private fun subscribeOnPushNotificationsPermission() {
viewModelScope.launch {
val isPushToggled = pushNotificationsFeatureToggles.isPushNotificationsEnabled
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
val isPushPermissionAvailable = getPushPermissionOrNull() != null
if (!isPushToggled || !shouldRequestPush || !isPushPermissionAvailable) return@launch
if (!shouldRequestPush || !isPushPermissionAvailable) return@launch
delay(timeMillis = 1_800)

View file

@ -89,9 +89,9 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.15-796"
tangemBlockchainSdk = "develop-797"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.15-384"
tangemCardSdk = "develop-385"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem16"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -126,6 +126,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"sei-network" -> Blockchain.Sei
"sei-network/test" -> Blockchain.SeiTestnet
"internet-computer" -> Blockchain.InternetComputer
"energy-web-chain" -> Blockchain.EnergyWebChain
"energy-web-chain/test" -> Blockchain.EnergyWebChainTestnet
"energy-web-x" -> Blockchain.EnergyWebX
"energy-web-x/test" -> Blockchain.EnergyWebXTestnet
else -> null
}
}
@ -253,6 +257,10 @@ fun Blockchain.toNetworkId(): String {
Blockchain.Sei -> "sei-network"
Blockchain.SeiTestnet -> "sei-network/test"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.EnergyWebChain -> "energy-web-chain"
Blockchain.EnergyWebChainTestnet -> "energy-web-chain/test"
Blockchain.EnergyWebX -> "energy-web-x"
Blockchain.EnergyWebXTestnet -> "energy-web-x/test"
}
}
@ -337,6 +345,8 @@ fun Blockchain.toCoinId(): String {
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum"
Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> "energy-web-chain"
Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-x"
}
}
@ -363,10 +373,12 @@ fun Blockchain.minimalAmount(): BigDecimal {
private const val NODL = "NODL"
private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
// no need to add testnets
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Sei,
Blockchain.InternetComputer,
Blockchain.EnergyWebChain,
Blockchain.EnergyWebX,
)