Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-27 09:19:28 +03:00
commit bab4923434
283 changed files with 1541 additions and 11004 deletions

View file

@ -1,4 +0,0 @@
{
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -1,4 +0,0 @@
{
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

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

View file

@ -718,6 +718,26 @@
"networkId": "sei-network/test" "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

@ -17,24 +17,23 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository 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.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase 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.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository 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.AppStateHolder
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -56,7 +55,7 @@ interface ApplicationEntryPoint {
fun getNetworkConnectionManager(): NetworkConnectionManager fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCustomTokenFeatureToggles(): CustomTokenFeatureToggles fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository fun getWalletConnect2Repository(): WalletConnect2Repository
@ -94,20 +93,16 @@ interface ApplicationEntryPoint {
fun getCardRepository(): CardRepository fun getCardRepository(): CardRepository
fun getFeedbackManagerFeatureToggles(): FeedbackManagerFeatureToggles
fun getTangemSdkLogger(): TangemSdkLogger fun getTangemSdkLogger(): TangemSdkLogger
fun getSettingsRepository(): SettingsRepository fun getSettingsRepository(): SettingsRepository
fun getBlockchainSDKFactory(): BlockchainSDKFactory fun getBlockchainSDKFactory(): BlockchainSDKFactory
fun getGetFeedbackEmailUseCase(): GetFeedbackEmailUseCase fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase
fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getGetCardInfoUseCase(): GetCardInfoUseCase fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener fun getUrlOpener(): UrlOpener
@ -116,7 +111,9 @@ interface ApplicationEntryPoint {
fun getAppRouter(): AppRouter fun getAppRouter(): AppRouter
fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles
fun getTangemAppLogger(): TangemAppLoggerInitializer fun getTangemAppLogger(): TangemAppLoggerInitializer
fun getHomeFeatureToggles(): HomeFeatureToggles
fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase
} }

View file

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

View file

@ -1,13 +1,10 @@
package com.tangem.tap package com.tangem.tap
import android.app.Application import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
@ -19,7 +16,6 @@ import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -30,36 +26,32 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository 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.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase 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.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository 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.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -96,8 +88,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val networkConnectionManager: NetworkConnectionManager private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager() get() = entryPoint.getNetworkConnectionManager()
private val customTokenFeatureToggles: CustomTokenFeatureToggles private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCustomTokenFeatureToggles() get() = entryPoint.getCardScanningFeatureToggles()
private val walletConnect2Repository: WalletConnect2Repository private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository() get() = entryPoint.getWalletConnect2Repository()
@ -153,9 +145,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val cardRepository: CardRepository private val cardRepository: CardRepository
get() = entryPoint.getCardRepository() get() = entryPoint.getCardRepository()
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
get() = entryPoint.getFeedbackManagerFeatureToggles()
private val tangemSdkLogger: TangemSdkLogger private val tangemSdkLogger: TangemSdkLogger
get() = entryPoint.getTangemSdkLogger() get() = entryPoint.getTangemSdkLogger()
@ -165,8 +154,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val blockchainSDKFactory: BlockchainSDKFactory private val blockchainSDKFactory: BlockchainSDKFactory
get() = entryPoint.getBlockchainSDKFactory() get() = entryPoint.getBlockchainSDKFactory()
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getGetFeedbackEmailUseCase() get() = entryPoint.getSendFeedbackEmailUseCase()
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase() get() = entryPoint.getSaveBlockchainErrorUseCase()
@ -174,9 +163,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val getCardInfoUseCase: GetCardInfoUseCase private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase() get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles()
private val urlOpener private val urlOpener
get() = entryPoint.getUrlOpener() get() = entryPoint.getUrlOpener()
@ -186,11 +172,14 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val appRouter: AppRouter private val appRouter: AppRouter
get() = entryPoint.getAppRouter() get() = entryPoint.getAppRouter()
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles
get() = entryPoint.getPushNotificationsFeatureToggles()
private val tangemAppLoggerInitializer: TangemAppLoggerInitializer private val tangemAppLoggerInitializer: TangemAppLoggerInitializer
get() = entryPoint.getTangemAppLogger() get() = entryPoint.getTangemAppLogger()
private val homeFeatureToggles: HomeFeatureToggles
get() = entryPoint.getHomeFeatureToggles()
private val getUserCountryUseCase: GetUserCountryUseCase
get() = entryPoint.getGetUserCountryCodeUseCase()
// endregion // endregion
override fun onCreate() { override fun onCreate() {
@ -212,10 +201,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// [REDACTED_JIRA] // [REDACTED_JIRA]
runBlocking { runBlocking {
featureTogglesManager.init() featureTogglesManager.init()
initConfigManager(
loader = FeaturesLocalLoader(assetLoader, BuildConfig.ENVIRONMENT), val config = configManager.initialize()
onComplete = ::initWithConfigDependency, store.dispatch(GlobalAction.SetConfigManager(configManager))
)
initWithConfigDependency(config = config)
} }
loadNativeLibraries() loadNativeLibraries()
@ -233,7 +223,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
) )
appStateHolder.mainStore = store appStateHolder.mainStore = store
walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) walletConnect2Repository.init(projectId = configManager.getConfigSync().walletConnectProjectId)
} }
private fun createReduxStore(): Store<AppState> { private fun createReduxStore(): Store<AppState> {
@ -243,7 +233,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
state = AppState( state = AppState(
daggerGraphState = DaggerGraphState( daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager, networkConnectionManager = networkConnectionManager,
customTokenFeatureToggles = customTokenFeatureToggles, cardScanningFeatureToggles = cardScanningFeatureToggles,
walletConnectRepository = walletConnect2Repository, walletConnectRepository = walletConnect2Repository,
walletConnectSessionsRepository = walletConnectSessionsRepository, walletConnectSessionsRepository = walletConnectSessionsRepository,
scanCardProcessor = scanCardProcessor, scanCardProcessor = scanCardProcessor,
@ -260,19 +250,18 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
generateWalletNameUseCase = generateWalletNameUseCase, generateWalletNameUseCase = generateWalletNameUseCase,
cardRepository = cardRepository, cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
tangemSdkLogger = tangemSdkLogger, tangemSdkLogger = tangemSdkLogger,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory, blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
getFeedbackEmailUseCase = getFeedbackEmailUseCase, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase, getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader, assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener, urlOpener = urlOpener,
shareManager = shareManager, shareManager = shareManager,
appRouter = appRouter, appRouter = appRouter,
pushNotificationsFeatureToggles = pushNotificationsFeatureToggles, homeFeatureToggles = homeFeatureToggles,
getUserCountryUseCase = getUserCountryUseCase,
), ),
), ),
) )
@ -289,16 +278,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
System.loadLibrary("TrustWalletCore") System.loadLibrary("TrustWalletCore")
} }
private suspend fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
configManager.load(loader) { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
onComplete(config)
}
}
private fun initWithConfigDependency(config: Config) { private fun initWithConfigDependency(config: Config) {
initAnalytics(this, config) initAnalytics(this, config)
initFeedbackManager(this, store) Log.addLogger(logger = tangemSdkLogger)
} }
private fun initAnalytics(application: Application, config: Config) { private fun initAnalytics(application: Application, config: Config) {
@ -318,51 +300,4 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
factory.build(Analytics, buildData) factory.build(Analytics, buildData)
// ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA] // ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA]
} }
private fun initFeedbackManager(context: Context, store: Store<AppState>) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo {
return AdditionalFeedbackInfo().apply {
appVersion = try {
// TODO don't use deprecated method
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
"x.y.z"
}
}
}
fun initTangemLogCollector(): TangemLogCollector {
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
Log.Level.Info,
)
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
}
val additionalFeedbackInfo = initAdditionalFeedbackInfo(context)
val tangemLogCollector = initTangemLogCollector()
Log.addLogger(
logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector,
)
val feedbackManager = LegacyFeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}
} }

View file

@ -1,91 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
*/
sealed class ManageTokens(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Manage Tokens", event, params) {
class ScreenOpened : ManageTokens("Manage Tokens Screen Opened")
class TokenSearched : ManageTokens("Token Searched")
class TokenSwitcherChanged(
type: AnalyticsParam.CurrencyType,
state: AnalyticsParam.OnOffState,
) : ManageTokens(
"Token Switcher Changed",
params = mapOf(
"Token" to type.value,
"State" to state.value,
),
)
class ButtonSaveChanges : ManageTokens("Button - Save Changes")
class ButtonCustomToken : ManageTokens("Button - Custom Token")
sealed class CustomToken(
event: String,
params: Map<String, String> = mapOf(),
) : ManageTokens(event, params) {
object ScreenOpened : ManageTokens(event = "Custom Token Screen Opened")
// TODO("Get rid of strong binding (CustomCurrency)
open class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens(
event = "Custom Token Was Added",
params = convertToParam(customCurrency),
) {
data class Token(
val symbol: String,
val derivationPath: String?,
val blockchain: com.tangem.blockchain.common.Blockchain,
val contractAddress: String,
) : ManageTokens(
event = "Custom Token Was Added",
params = mapOf(
"Token" to symbol,
"Derivation Path" to derivationPath,
"Network Id" to blockchain.currency,
"Contract Address" to contractAddress,
).filterNotNull(),
)
data class Blockchain(
val derivationPath: String?,
val blockchain: com.tangem.blockchain.common.Blockchain,
) : ManageTokens(
event = "Custom Token Was Added",
params = mapOf(
"Token" to blockchain.currency,
"Derivation Path" to derivationPath,
).filterNotNull(),
)
companion object {
private fun convertToParam(customCurrency: CustomCurrency): Map<String, String> = with(customCurrency) {
return when (this) {
is CustomCurrency.CustomBlockchain -> mapOf(
"Token" to network.currency,
"Derivation Path" to derivationPath?.rawPath,
).filterNotNull()
is CustomCurrency.CustomToken -> mapOf(
"Token" to token.symbol,
"Derivation Path" to derivationPath?.rawPath,
"Network Id" to network.toNetworkId(),
"Contract Address" to token.contractAddress,
).filterNotNull()
}
}
}
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.analytics.events package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.ui.details.SocialNetwork
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -14,20 +13,8 @@ sealed class Settings(
) : AnalyticsEvent(category, event, params, error) { ) : AnalyticsEvent(category, event, params, error) {
class ScreenOpened : Settings(event = "Settings Screen Opened") 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 ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session")
class ButtonStopWalletConnectSession : Settings(event = "Button - Stop 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( sealed class CardSettings(
event: String, event: String,
@ -63,8 +50,6 @@ sealed class Settings(
error = error, error = error,
) )
class AccessCodeRecoveryButton : CardSettings("Button - Access Code Recovery")
class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings( class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings(
event = "Access Code Recovery Changed", event = "Access Code Recovery Changed",
params = mapOf(status.key to status.value), params = mapOf(status.key to status.value),

View file

@ -21,7 +21,3 @@ fun BigDecimal.toFormattedString(
df.roundingMode = roundingMode df.roundingMode = roundingMode
return df.format(this) return df.format(this)
} }
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
fun BigDecimal.isPositive(): Boolean = this.signum() == 1

View file

@ -1,147 +0,0 @@
package com.tangem.tap.common.feedback
import android.os.Build
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.crypto.NetworkType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import java.util.concurrent.CopyOnWriteArrayList
class AdditionalFeedbackInfo {
class EmailWalletInfo(
val blockchain: Blockchain = Blockchain.Unknown,
val derivationPath: String = "",
val outputsCount: String? = null,
val host: String = "",
val addresses: String = "",
val explorerLink: String = "",
val tokens: List<EmailTokenInfo> = emptyList(),
)
class EmailTokenInfo(
val id: String?,
val name: String,
val contractAddress: String,
)
var appVersion: String = ""
// card
var cardId: String = ""
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
var userWalletId: String = ""
var extendedPublicKey: String = ""
// wallets
val walletsInfo = CopyOnWriteArrayList<EmailWalletInfo>()
var onSendErrorWalletInfo: EmailWalletInfo? = null
private set
var signedHashesCount: String = ""
private set
// device
var phoneModel: String = Build.MODEL
private set
var osVersion: String = Build.VERSION.SDK_INT.toString()
private set
// send error
var destinationAddress: String = ""
private set
var amount: String = ""
private set
var fee: String = ""
private set
var token: String = ""
private set
private val Address.name: String
get() = type.javaClass.simpleName
@Deprecated("Don't use it directly")
fun setCardInfo(data: ScanResponse) {
cardId = data.card.cardId
cardBlockchain = data.walletData?.blockchain ?: ""
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
extendedPublicKey = runCatching {
data.card.wallets.firstOrNull { it.extendedPublicKey != null }
?.extendedPublicKey
?.serialize(networkType = NetworkType.Mainnet).orEmpty()
}.getOrDefault("couldn't retrieve extended public key")
}
@Deprecated("Don't use it directly")
fun setWalletsInfo(walletManagers: List<WalletManager>) {
synchronized(walletsInfo) {
walletsInfo.clear()
walletsInfo.addAll(elements = walletManagers.map(::createEmailWalletInfo))
}
}
fun updateOnSendError(
walletManager: WalletManager,
amountToSend: Amount?,
feeAmount: Amount?,
destinationAddress: String?,
) {
onSendErrorWalletInfo = createEmailWalletInfo(walletManager)
this.destinationAddress = destinationAddress ?: "0"
amount = amountToSend?.value?.stripZeroPlainString() ?: "0"
fee = if (feeAmount != null) {
feeAmount.value?.stripZeroPlainString() ?: "0"
} else {
"Unable to receive"
}
token = if (amountToSend?.type is AmountType.Token) amountToSend.currencySymbol else ""
}
private fun createEmailWalletInfo(walletManager: WalletManager): EmailWalletInfo {
return EmailWalletInfo(
blockchain = walletManager.wallet.blockchain,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
outputsCount = walletManager.outputsCount?.toString(),
host = walletManager.currentHost,
addresses = formatAddresses(walletManager.wallet),
explorerLink = formatExploreUrls(walletManager.wallet),
tokens = walletManager.cardTokens.map { token ->
EmailTokenInfo(token.id, token.name, token.contractAddress)
},
)
}
private fun formatSignedHashes(wallets: List<CardDTO.Wallet>): String {
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
}
private fun formatAddresses(wallet: Wallet): String {
return wallet.formatAddressWith("Multiple address:") {
"${it.name} - ${it.value}"
}
}
private fun formatExploreUrls(wallet: Wallet): String {
return wallet.formatAddressWith("Multiple explorers links:") {
"${it.name} - ${wallet.getExploreUrl(it.value)}"
}
}
@Suppress("MagicNumber")
private fun Wallet.formatAddressWith(with: String, mapAddress: (Address) -> String): String {
return if (addresses.size == 1) {
getExploreUrl(address)
} else {
addresses.map { mapAddress(it) }.toMutableList()
.apply { add(0, with) }
.joinToString("\n")
}
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.common.feedback
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
internal class DefaultFeedbackManagerFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : FeedbackManagerFeatureToggles {
override val isLocalLogsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "LOCAL_USER_LOGS_ENABLED")
}

View file

@ -1,105 +0,0 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.breakLine
import com.tangem.wallet.R
interface FeedbackData {
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalFeedbackInfo) {}
fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String
@Suppress("MagicNumber")
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
breakLine(3)
append(context.getString(getDataCollectionMessageResId()))
breakLine()
append(createOptionalMessage(infoHolder))
}.toString()
}
}
class RateCanBeBetterEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_rate_negative
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.breakLine()
.appendPhoneInfo()
.build()
}
class ScanFailsEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_scan_failed
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
@Suppress("MagicNumber")
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
append(context.getString(mainMessageResId))
breakLine(4)
append(createOptionalMessage(infoHolder))
}.toString()
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendPhoneInfo()
.build()
}
class SendTransactionFailedEmail(
val error: String,
) : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_tx_failed
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendTxFailedBlockchainInfo(error)
.breakLine()
.appendPhoneInfo()
.build()
}
class FeedbackEmail : FeedbackData {
override val subjectResId: Int
get() = if (isS2CCard) R.string.feedback_subject_support else R.string.feedback_subject_support_tangem
override val mainMessageResId: Int = R.string.feedback_preface_support
private var isS2CCard = false
override fun prepare(infoHolder: AdditionalFeedbackInfo) {
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
}
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendWalletsInfo()
.breakLine()
.appendPhoneInfo()
.build()
}
class SupportInfo : FeedbackData {
override val subjectResId: Int = R.string.details_chat
override val mainMessageResId: Int = R.string.details_chat
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
return FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendPhoneInfo()
.build()
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.tap.common.feedback
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
private val infoHolder: AdditionalFeedbackInfo,
) {
val builder = StringBuilder()
fun appendDelimiter(): FeedbackDataBuilder {
builder.appendDelimiter()
return this
}
fun breakLine(count: Int = 1): FeedbackDataBuilder {
builder.breakLine(count)
return this
}
fun appendCardInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("", infoHolder.signedHashesCount)
builder.appendKeyValue("User Wallet ID", infoHolder.userWalletId)
return this
}
fun appendWalletsInfo(): FeedbackDataBuilder {
infoHolder.walletsInfo.forEach { walletInfo ->
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Outputs count", walletInfo.outputsCount)
if (walletInfo.tokens.isNotEmpty()) {
builder.append("Tokens:")
breakLine()
walletInfo.tokens.forEach { token ->
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Wallet address", walletInfo.addresses)
builder.appendKeyValue("Explorer link", walletInfo.explorerLink)
}
return this
}
fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)
builder.appendDelimiter()
builder.appendKeyValue("Source address", walletInfo.addresses)
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
builder.appendKeyValue("Amount", infoHolder.amount)
builder.appendKeyValue("Fee", infoHolder.fee)
return this
}
fun appendPhoneInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return this
}
fun build(): String = builder.toString()
}
private fun StringBuilder.appendKeyValue(key: String, value: String?): StringBuilder = when {
value.isNullOrBlank() -> this
key.isBlank() -> this.append("$value\n")
else -> this.append("$key: $value\n")
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")

View file

@ -1,136 +0,0 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.withForegroundActivity
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class LegacyFeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase,
) {
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) {
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
scope.launch {
val getCardInfo = suspend {
scanResponse ?: error("ScanResponse must be not null")
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
}
val email = getFeedbackEmailUseCase(
type = when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo())
is ScanFailsEmail -> FeedbackEmailType.ScanningProblem
is SendTransactionFailedEmail -> {
FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo())
}
else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
},
)
store.inject(DaggerGraphState::emailSender).send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
} else {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity),
)
}
}
}
fun sendEmail(type: FeedbackEmailType) {
if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled")
scope.launch {
val email = getFeedbackEmailUseCase(type = type)
store.inject(DaggerGraphState::emailSender).send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
}
private fun getLogFile(context: Context): File? {
return try {
if (sessionLogsFile != null) {
return sessionLogsFile
}
val file = File(context.filesDir, LOGS_FILE)
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logCollector.getLogs().forEach { stringWriter.append(it) }
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
if (file.exists()) {
sessionLogsFile = file
sessionLogsFile
} else {
null
}
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
}
}
private fun getSupportEmail(): String {
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
S2C_SUPPORT_EMAIL
} else {
DEFAULT_SUPPORT_EMAIL
}
}
private companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
const val LOGS_FILE = "logs.txt"
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.feedback
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.store
import timber.log.Timber
internal class ProxyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackEmailType) {
val manager = store.state.globalState.feedbackManager
if (manager == null) {
Timber.e("Feedback manager is not initialized")
return
}
manager.sendEmail(type)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.common.log
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import java.text.SimpleDateFormat
import java.util.*
class TangemLogCollector(
private val levels: List<Log.Level>,
private val messageFormatter: LogFormat,
) : TangemSdkLogger {
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
private val logs = mutableListOf<String>()
private val mutex = Object()
override fun log(message: () -> String, level: Log.Level) {
if (!levels.contains(level)) return
synchronized(mutex) {
val formattedMessage = messageFormatter.format(message, level)
val logMessage = "${dateFormatter.format(Date())}: $formattedMessage"
logs.add("$logMessage\n")
}
}
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
fun clearLogs() = synchronized(mutex) { logs.clear() }
}

View file

@ -3,14 +3,12 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer 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.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer
@ -28,8 +26,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state), onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state), twinCardsState = TwinCardsReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state), welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state), saveWalletState = SaveWalletReducer.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.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState 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.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
@ -25,7 +23,6 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.welcome.redux.WelcomeState
@ -43,8 +40,6 @@ data class AppState(
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(), val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(), val twinCardsState: TwinCardsState = TwinCardsState(),
val detailsState: DetailsState = DetailsState(), val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(), val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(), val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(), val saveWalletState: SaveWalletState = SaveWalletState(),
@ -75,7 +70,6 @@ data class AppState(
OnboardingOtherCardsMiddleware.handler, OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler, TwinCardsMiddleware.handler,
DetailsMiddleware().detailsMiddleware, DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware, WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware, BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware, WelcomeMiddleware().middleware,

View file

@ -6,8 +6,6 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.NotificationAction
@ -70,9 +68,6 @@ sealed class GlobalAction : Action {
) : GlobalAction() ) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
object ExchangeManager : GlobalAction() { object ExchangeManager : GlobalAction() {
object Init : GlobalAction() { object Init : GlobalAction() {

View file

@ -55,15 +55,9 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
is GlobalAction.RestoreAppCurrency -> { is GlobalAction.RestoreAppCurrency -> {
restoreAppCurrency() restoreAppCurrency()
} }
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = action.feedbackData,
scanResponse = action.scanResponse,
)
}
is GlobalAction.ExchangeManager.Init -> { is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config ?: return val config = appStateSafe.globalState.configManager?.getConfigSync() ?: return
scope.launch { scope.launch {
val scanResponseProvider: () -> ScanResponse? = { val scanResponseProvider: () -> ScanResponse? = {
@ -92,21 +86,24 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
scope.launch { exchangeManager.update() } scope.launch { exchangeManager.update() }
} }
is GlobalAction.FetchUserCountry -> { is GlobalAction.FetchUserCountry -> {
scope.launch { val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
// TODO("After adding DI") get dependencies by DI
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() } if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
.onSuccess { scope.launch {
store.dispatchOnMain( runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()), .onSuccess {
) store.dispatchOnMain(
} GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
.onFailure { )
store.dispatchOnMain( }
GlobalAction.FetchUserCountry.Success( .onFailure {
countryCode = Locale.getDefault().country.lowercase(), 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.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction 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.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.extensions.replaceBy import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action import org.rekotlin.Action
@ -71,9 +68,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
) )
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance)) globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
} }
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}
is GlobalAction.ShowDialog -> { is GlobalAction.ShowDialog -> {
globalState.copy(dialog = action.stateDialog) globalState.copy(dialog = action.stateDialog)
} }
@ -86,10 +80,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
is GlobalAction.SetIfCardVerifiedOnline -> is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified) globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> { is GlobalAction.FetchUserCountry.Success -> {
store.dispatchOnMain(HomeAction.UpdateCountryCode(action.countryCode)) globalState.copy(userCountryCode = action.countryCode)
globalState.copy(
userCountryCode = action.countryCode,
)
} }
else -> globalState else -> globalState
} }

View file

@ -5,7 +5,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -18,11 +17,11 @@ data class GlobalState(
val cardVerifiedOnline: Boolean = false, val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(), val tapWalletManager: TapWalletManager = TapWalletManager(),
val configManager: ConfigManager? = null, val configManager: ConfigManager? = null,
val feedbackManager: LegacyFeedbackManager? = null,
val appCurrency: AppCurrency = AppCurrency.Default, val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0, val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null, val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
@Deprecated("Use GetUserCountryCodeUseCase instead")
val userCountryCode: String? = null, val userCountryCode: String? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType ) : StateType

View file

@ -1,15 +1,8 @@
package com.tangem.tap.common.redux.legacy package com.tangem.tap.common.redux.legacy
import com.tangem.blockchain.common.AmountType
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsAction
@ -23,7 +16,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Middleware import org.rekotlin.Middleware
internal object LegacyMiddleware { internal object LegacyMiddleware {
@ -33,68 +25,13 @@ internal object LegacyMiddleware {
{ next -> { next ->
{ action -> { action ->
when (action) { when (action) {
is LegacyAction.SendEmailRateCanBeBetter -> {
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = RateCanBeBetterEmail(),
scanResponse = action.scanResponse,
)
}
is LegacyAction.SendEmailSupport -> {
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = FeedbackEmail(),
scanResponse = action.scanResponse,
)
}
is LegacyAction.StartOnboardingProcess -> { is LegacyAction.StartOnboardingProcess -> {
store.dispatch( store.dispatch(
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), GlobalAction.Onboarding.Start(
)
}
is LegacyAction.SendEmailTransactionFailed -> {
if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
val amount = action.amount?.convertToSdkAmount(action.cryptoCurrency)
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
error = BlockchainErrorInfo(
errorMessage = action.errorMessage,
blockchainId = action.cryptoCurrency.network.id.value,
derivationPath = action.cryptoCurrency.network.derivationPath.value,
destinationAddress = action.destinationAddress.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
""
},
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
fee = action.fee?.convertToSdkAmount(action.cryptoCurrency)
?.value?.stripZeroPlainString() ?: "unknown",
),
)
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = action.scanResponse, scanResponse = action.scanResponse,
) canSkipBackup = action.canSkipBackup,
} else { ),
scope.launch { )
store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWalletId,
network = action.cryptoCurrency.network,
)?.let { walletManager ->
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
walletManager = walletManager,
amountToSend = action.amount?.convertToSdkAmount(action.cryptoCurrency),
feeAmount = action.fee?.convertToSdkAmount(action.cryptoCurrency),
destinationAddress = action.destinationAddress,
)
}
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = null,
)
}
}
} }
is LegacyAction.PrepareDetailsScreen -> { is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)

View file

@ -8,16 +8,19 @@ import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.launch
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -65,7 +68,11 @@ internal object ScanFailsDialog {
} }
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
Analytics.send(Basic.ButtonSupport(sourceAnalytics)) Analytics.send(Basic.ButtonSupport(sourceAnalytics))
store.dispatch(GlobalAction.SendEmail(feedbackData = ScanFailsEmail(), scanResponse = null))
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.ScanningProblem)
}
} }
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
store.dispatchDialogHide() store.dispatchDialogHide()

View file

@ -1,27 +1,34 @@
package com.tangem.core.navigation.email package com.tangem.tap.core.navigation.email
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.foregroundActivityObserver
import timber.log.Timber import timber.log.Timber
/** /**
* Implementation of email sender for Android * Implementation of email sender for Android
* *
* @property context context
*
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class AndroidEmailSender(private val context: Context) : EmailSender { internal class AndroidEmailSender : EmailSender {
override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) { override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) {
val originalIntent = createEmailShareIntent(email) val activity = foregroundActivityObserver.foregroundActivity
if (activity == null) {
Timber.e("Foreground activity not found")
return
}
val originalIntent = createEmailShareIntent(activity, email)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val packageManager = context.packageManager val packageManager = activity.packageManager
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0) val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0) val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
@ -32,7 +39,7 @@ internal class AndroidEmailSender(private val context: Context) : EmailSender {
} }
} }
.map { .map {
createEmailShareIntent(email).apply { setPackage(it.activityInfo.packageName) } createEmailShareIntent(activity, email).apply { setPackage(it.activityInfo.packageName) }
} }
.toMutableList() .toMutableList()
@ -41,14 +48,14 @@ internal class AndroidEmailSender(private val context: Context) : EmailSender {
putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
} }
ContextCompat.startActivity(context, chooserIntent, null) ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e("Failed to send email: $ex") Timber.e("Failed to send email: $ex")
} }
} }
private fun createEmailShareIntent(email: EmailSender.Email): Intent { private fun createEmailShareIntent(activity: AppCompatActivity, email: EmailSender.Email): Intent {
val builder = ShareCompat.IntentBuilder(context) val builder = ShareCompat.IntentBuilder(activity)
.setType("message/rfc822") .setType("message/rfc822")
.setEmailTo(arrayOf(email.address)) .setEmailTo(arrayOf(email.address))
.setSubject(email.subject) .setSubject(email.subject)
@ -56,7 +63,7 @@ internal class AndroidEmailSender(private val context: Context) : EmailSender {
email.attachment?.let { email.attachment?.let {
builder.setStream( builder.setStream(
FileProvider.getUriForFile(context, "${context.packageName}.provider", it), FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it),
) )
} }

View file

@ -1,23 +0,0 @@
package com.tangem.tap.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.tap.common.feedback.DefaultFeedbackManagerFeatureToggles
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 FeedbackManagerFeatureTogglesModule {
@Provides
@Singleton
fun provideFeedbackManagerFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): FeedbackManagerFeatureToggles {
return DefaultFeedbackManagerFeatureToggles(featureTogglesManager)
}
}

View file

@ -5,8 +5,6 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.tap.common.feedback.ProxyFeedbackManager
import com.tangem.tap.common.finisher.AndroidAppFinisher import com.tangem.tap.common.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager import com.tangem.tap.common.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager import com.tangem.tap.common.share.IntentShareManager
@ -30,10 +28,6 @@ internal object UtilsModule {
@Singleton @Singleton
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener() fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
@Provides
@Singleton
fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager()
@Provides @Provides
@Singleton @Singleton
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.core.navigation.email
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.core.navigation.email.AndroidEmailSender
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 EmailSenderModule {
@Provides
@Singleton
fun provideEmailSender(): EmailSender = AndroidEmailSender()
}

View file

@ -22,14 +22,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object CardDomainModule { internal object CardDomainModule {
@Provides
@Singleton
fun provideGetBiometricsStatusUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): GetBiometricsStatusUseCase {
return GetBiometricsStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideSetAccessCodeRequestPolicyUseCase( fun provideSetAccessCodeRequestPolicyUseCase(
@ -38,12 +30,6 @@ internal object CardDomainModule {
return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
} }
@Provides
@Singleton
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
return WasCardScannedUseCase(cardRepository = cardRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase { fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {

View file

@ -1,8 +1,10 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.card.* import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -14,6 +16,12 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object CardLegacyDomainModule { internal object CardLegacyDomainModule {
@Provides
@Singleton
fun provideCardScanningFeatureToggles(featureTogglesManager: FeatureTogglesManager): CardScanningFeatureToggles {
return CardScanningFeatureToggles(featureTogglesManager)
}
@Provides @Provides
@Singleton @Singleton
fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor() fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor()

View file

@ -2,8 +2,8 @@ package com.tangem.tap.di.domain
import android.content.Context import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -27,8 +27,8 @@ internal object FeedbackDomainModule {
fun provideGetFeedbackEmailUseCase( fun provideGetFeedbackEmailUseCase(
feedbackRepository: FeedbackRepository, feedbackRepository: FeedbackRepository,
@ApplicationContext context: Context, @ApplicationContext context: Context,
): GetFeedbackEmailUseCase { ): SendFeedbackEmailUseCase {
return GetFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) return SendFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
} }
@Provides @Provides

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.PermissionRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository 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.sdk.TangemSdkManager
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
import dagger.Module import dagger.Module
@ -212,4 +214,16 @@ internal object SettingsDomainModule {
return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository) return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository)
} }
// endregion // endregion
@Provides
@Singleton
fun provideFetchUserCountryCodeUseCase(settingsRepository: SettingsRepository): FetchUserCountryUseCase {
return FetchUserCountryUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideGetUserCountryCodeUseCase(settingsRepository: SettingsRepository): GetUserCountryUseCase {
return GetUserCountryUseCase(settingsRepository)
}
} }

View file

@ -287,12 +287,6 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase( fun provideIsCryptoCurrencyCoinCouldHideUseCase(
@ -313,68 +307,6 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun provideHasMissedAddressesCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
): GetMissedAddressesCryptoCurrenciesUseCase {
return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
@Provides
@Singleton
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
return GetGlobalTokenListUseCase(repository = tokensListRepository)
}
@Provides
@Singleton
fun provideCheckTokenCompatibilityUseCase(
networksCompatibilityRepository: NetworksCompatibilityRepository,
): CheckCurrencyCompatibilityUseCase {
return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository)
}
@Provides
@Singleton
fun provideNeedHardenedDerivationUseCase(
networksCompatibilityRepository: NetworksCompatibilityRepository,
): RequiresHardenedDerivationOnlyUseCase {
return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository)
}
@Provides
@Singleton
fun provideFindTokenByContractAddressUseCase(
tokensListRepository: TokensListRepository,
): FindTokenByContractAddressUseCase {
return FindTokenByContractAddressUseCase(repository = tokensListRepository)
}
@Provides
@Singleton
fun provideValidateContractAddressUseCase(
tokensListRepository: TokensListRepository,
): ValidateContractAddressUseCase {
return ValidateContractAddressUseCase(tokensListRepository = tokensListRepository)
}
@Provides
@Singleton
fun provideAreTokensSupportedByNetworkUseCase(
repository: NetworksCompatibilityRepository,
): AreTokensSupportedByNetworkUseCase {
return AreTokensSupportedByNetworkUseCase(repository = repository)
}
@Provides
@Singleton
fun provideGetNetworksSupportedByWallet(
repository: NetworksCompatibilityRepository,
): GetNetworksSupportedByWallet {
return GetNetworksSupportedByWallet(repository = repository)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetBalanceNotEnoughForFeeWarningUseCase( fun provideGetBalanceNotEnoughForFeeWarningUseCase(

View file

@ -4,19 +4,12 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.Wallet
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation 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.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction 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.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -54,30 +47,13 @@ class TapWalletManager(
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
if (!featureToggles.isLocalLogsEnabled) {
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
}
updateConfigManager(scanResponse)
withMainContext { withMainContext {
// Order is important // Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
} }
} }
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
if (data.cardTypesResolver.isStart2Coin()) {
configManager?.turnOff(ConfigManager.IS_TOP_UP_ENABLED)
} else {
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
} }
fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0) fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0)

View file

@ -0,0 +1,16 @@
package com.tangem.tap.domain.scanCard
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
/**
* Add custom token feature toggles
*
[REDACTED_AUTHOR]
*/
class CardScanningFeatureToggles internal constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isNewCardScanningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
}

View file

@ -12,7 +12,7 @@ import com.tangem.tap.store
// TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead // TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead
internal class DefaultScanCardProcessor : ScanCardProcessor { internal class DefaultScanCardProcessor : ScanCardProcessor {
private val isNewCardScanningEnabled: Boolean private val isNewCardScanningEnabled: Boolean
get() = store.inject(DaggerGraphState::customTokenFeatureToggles).isNewCardScanningEnabled get() = store.inject(DaggerGraphState::cardScanningFeatureToggles).isNewCardScanningEnabled
override suspend fun scan( override suspend fun scan(
cardId: String?, cardId: String?,

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute 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.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer 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.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep 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( private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse, scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {}, crossinline disclaimerWillShow: () -> Unit = {},
@ -109,7 +107,6 @@ internal object LegacyScanProcessor {
crossinline onFailure: suspend (error: TangemError) -> Unit, crossinline onFailure: suspend (error: TangemError) -> Unit,
) { ) {
val disclaimer = scanResponse.card.createDisclaimer() val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) { if (disclaimer.isAccepted()) {
nextHandler(scanResponse) nextHandler(scanResponse)
@ -120,23 +117,9 @@ internal object LegacyScanProcessor {
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
disclaimerWillShow() disclaimerWillShow()
store.dispatch( store.dispatchNavigationAction {
DisclaimerAction.Show( push(AppRoute.Disclaimer(isTosAccepted = false))
from = DisclaimerSource.Home, }
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main.immediate) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main.immediate) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
)
} }
} }
} }
@ -149,10 +132,6 @@ internal object LegacyScanProcessor {
crossinline onWalletNotCreated: suspend () -> Unit, crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit,
) { ) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) { if (OnboardingHelper.isOnboardingCase(scanResponse)) {

View file

@ -59,12 +59,8 @@ internal object UseCaseScanProcessor {
), ),
) )
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource))) add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
val pushNotificationsToggles = add(DisclaimerChain(store, disclaimerWillShow))
store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) add(CheckForOnboardingChain(store))
if (pushNotificationsToggles.isPushNotificationsEnabled) {
add(DisclaimerChain(store, disclaimerWillShow))
}
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
} }
scanCardUseCase(cardId, afterScanChains = chains).fold( scanCardUseCase(cardId, afterScanChains = chains).fold(

View file

@ -16,7 +16,6 @@ import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingHelper 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.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
@ -32,17 +31,14 @@ import org.rekotlin.Store
* - [ScanChainException.OnboardingNeeded] if onboarding required. * - [ScanChainException.OnboardingNeeded] if onboarding required.
* *
* @param store the [Store] that holds the state of the app. * @param store the [Store] that holds the state of the app.
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
* *
* @see Chain for more information about the Chain interface. * @see Chain for more information about the Chain interface.
*/ */
class CheckForOnboardingChain( class CheckForOnboardingChain(
private val store: Store<AppState>, private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
) : ResultChain<ScanCardException, ScanResponse>() { ) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when { return when {

View file

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

View file

@ -4,12 +4,12 @@ import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.calculateSha256
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
import com.tangem.tap.features.details.redux.walletconnect.TradeData import com.tangem.tap.features.details.redux.walletconnect.TradeData
import com.tangem.utils.extensions.stripZeroPlainString
import timber.log.Timber import timber.log.Timber
internal object BnbHelper { internal object BnbHelper {

View file

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

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.customtoken.api.featuretoggles
/**
* Add custom token feature toggles
*
[REDACTED_AUTHOR]
*/
interface CustomTokenFeatureToggles {
val isNewCardScanningEnabled: Boolean
}

View file

@ -1,61 +0,0 @@
package com.tangem.tap.features.customtoken.impl.data
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
/**
* Default implementation of custom token repository
*
* @property tangemTechApi TangemTech API
* @property dispatchers coroutine dispatchers provider
* @property reduxStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
class DefaultCustomTokenRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: AppStateHolder,
) : CustomTokenRepository {
override suspend fun findToken(address: String, networkId: String?): FoundToken {
val scanResponse = requireNotNull(reduxStateHolder.scanResponse)
val supportedTokenNetworkIds = requireNotNull(scanResponse.card)
.supportedBlockchains(scanResponse.cardTypesResolver)
.filter(Blockchain::canHandleTokens)
.map(Blockchain::toNetworkId)
return withContext(dispatchers.io) {
val foundCoin = tangemTechApi.getCoins(
contractAddress = address,
networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","),
)
.getOrThrow()
.coins.firstNotNullOfOrNull { coin ->
val networksWithTheSameAddress = coin.networks.filter { network ->
(network.contractAddress != null || network.decimalCount != null) &&
network.contractAddress?.equals(address, ignoreCase = true) == true &&
supportedTokenNetworkIds.contains(network.networkId)
}
if (networksWithTheSameAddress.isNotEmpty()) {
coin.copy(networks = networksWithTheSameAddress)
} else {
null
}
}
foundCoin?.let(FoundTokenConverter::convert) ?: error("Token not found")
}
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.customtoken.impl.data.converters
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.utils.converter.Converter
/**
* Converter between data model [CoinsResponse.Coin] and domain model [FoundToken]
*
[REDACTED_AUTHOR]
*/
object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
override fun convert(value: CoinsResponse.Coin): FoundToken {
return FoundToken(
id = value.id,
name = value.name,
symbol = value.symbol,
isActive = value.active,
network = value.networks.firstOrNull()?.let { network ->
FoundToken.Network(
id = network.networkId,
contractAddress = requireNotNull(network.contractAddress),
decimalCount = requireNotNull(network.decimalCount).toString(),
)
} ?: error("Found token networks is empty"),
)
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.customtoken.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.customtoken.impl.featuretoggles.DefaultCustomTokenFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object CustomTokenFeatureTogglesModule {
@Provides
@Singleton
fun providesCustomTokenFeatureToggles(featureTogglesManager: FeatureTogglesManager): CustomTokenFeatureToggles {
return DefaultCustomTokenFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(ViewModelComponent::class)
internal object CustomTokenInteractorModule {
@Provides
@ViewModelScoped
fun provideCustomTokenInteractor(
tangemTechApi: TangemTechApi,
appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
reduxStateHolder: AppStateHolder,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
tangemTechApi = tangemTechApi,
dispatchers = appCoroutineDispatcherProvider,
reduxStateHolder = reduxStateHolder,
),
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.customtoken.impl.di
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
import com.tangem.tap.features.customtoken.impl.presentation.routers.DefaultCustomTokenRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(ViewModelComponent::class)
internal object CustomTokenRouterModule {
@Provides
@ViewModelScoped
fun provideAddCustomTokenRouter(): CustomTokenRouter = DefaultCustomTokenRouter()
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.core.TangemError
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
/**
* Custom token interactor
*
[REDACTED_AUTHOR]
*/
interface CustomTokenInteractor {
/** Find token by [address] and [blockchain] */
suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
/** Save token [customCurrency] */
@Throws(TangemError::class)
suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit>
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
/**
* Custom token repository
*
[REDACTED_AUTHOR]
*/
interface CustomTokenRepository {
/**
* Find token by [address] and [networkId]
*
* @throws com.tangem.datasource.api.common.response.ApiResponseError
* */
suspend fun findToken(address: String, networkId: String?): FoundToken
}

View file

@ -1,84 +0,0 @@
package com.tangem.tap.features.customtoken.impl.domain
import arrow.core.getOrElse
import arrow.core.raise.result
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
/**
* Default implementation of custom token interactor
*
* @property featureRepository feature repository
*
[REDACTED_AUTHOR]
*/
class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
) : CustomTokenInteractor {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository)
val networksRepository = store.inject(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
return featureRepository.findToken(
address = address,
networkId = if (blockchain != Blockchain.Unknown) blockchain.toNetworkId() else null,
)
}
override suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit> {
return result {
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Failed to get selected wallet: $it")
}
val currency = Currency.fromCustomCurrency(customCurrency)
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
}
}
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
return when (this) {
is Currency.Blockchain -> {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
is Currency.Token -> {
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
}
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.customtoken.impl.domain.models
/**
* Found token model
*
* @property id id
* @property name name
* @property symbol symbol
* @property isActive flag that determines status of token
* @property network network
*
[REDACTED_AUTHOR]
*/
data class FoundToken(
val id: String,
val name: String,
val symbol: String,
val isActive: Boolean,
val network: Network,
) {
/**
* Found token network
*
* @property id id
* @property contractAddress address
* @property decimalCount decimal count
*/
data class Network(val id: String, val contractAddress: String, val decimalCount: String)
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.customtoken.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
/**
* Default implementation of CustomToken feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
*
[REDACTED_AUTHOR]
*/
internal class DefaultCustomTokenFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : CustomTokenFeatureToggles {
override val isNewCardScanningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
}

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Add custom token screen
*
[REDACTED_AUTHOR]
*/
@AndroidEntryPoint
internal class AddCustomTokenFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AddCustomTokenScreen(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
stateHolder = state,
)
}
}

View file

@ -1,324 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.models
import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
/**
* Toolbar model of add custom token screen
*
* @property title title
* @property onBackButtonClick lambda be invoked when back button is been pressed
*/
internal data class AddCustomTokensToolbar(val title: TextReference, val onBackButtonClick: () -> Unit)
/**
* Model of block with fields for testing
*
* @property chooseTokenButtonText choose token button text
* @property clearButtonText clear button text
* @property resetButtonText reset button text
* @property onClearAddressButtonClick lambda be invoked when clear address button is been pressed
* @property onResetButtonClick lambda be invoked when reset form fields button is been pressed
*/
internal data class AddCustomTokenTestBlock(
val chooseTokenButtonText: String,
val clearButtonText: String,
val resetButtonText: String,
val onClearAddressButtonClick: () -> Unit,
val onResetButtonClick: () -> Unit,
)
/**
* Bottom sheet model for choose custom token
*
* @property categoriesBlocks tokens categories
* @property onTestTokenClick lambda be invoked when token is been pressed
*/
internal data class AddCustomTokenChooseTokenBottomSheet(
val categoriesBlocks: List<TokensCategoryBlock>,
val onTestTokenClick: (String) -> Unit,
) {
/**
* Tokens category model
*
* @property name category name
* @property items category items
*/
data class TokensCategoryBlock(val name: String, val items: List<TestTokenItem>)
/**
* Test token model
*
* @property name token name
* @property address token address
*/
data class TestTokenItem(val name: String, val address: String)
}
/**
* Form with fields model of add custom token screen
*
* @property contractAddressInputField input field to enter the contract address
* @property networkSelectorField selector field to select the token network
* @property tokenNameInputField input field to enter the token name
* @property tokenSymbolInputField input field to enter the token symbol
* @property decimalsInputField input field to enter the token decimals
* @property derivationPathSelectorField selector field to select the derivation path
* @property derivationPathInputField input field for a custom derivation path
* @property showTokenFields if token fields should be shown
*/
internal data class AddCustomTokenForm(
val contractAddressInputField: AddCustomTokenInputField.ContactAddress,
val networkSelectorField: AddCustomTokenSelectorField.Network,
val tokenNameInputField: AddCustomTokenInputField.TokenName,
val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol,
val decimalsInputField: AddCustomTokenInputField.Decimals,
val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?,
val derivationPathInputField: AddCustomTokenInputField.DerivationPath?,
val showTokenFields: Boolean = false,
)
/** Base input field model of add custom token screen */
internal sealed interface AddCustomTokenInputField {
/** Current value */
val value: String
/** Lambda be invoked when value is been changed */
val onValueChange: (String) -> Unit
/** Keyboard options */
val keyboardOptions: KeyboardOptions
/** Label */
val label: TextReference
/** Placeholder (hint) */
val placeholder: TextReference
/**
* Input field model to enter the contract address
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isLoading flag that determine the processing of current value
* @property isError flag that determine if current value has error
* @property error error description
*/
data class ContactAddress(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isLoading: Boolean,
val isError: Boolean,
val error: TextReference? = null,
) : AddCustomTokenInputField
/**
* Input field model to enter the token name
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
data class TokenName(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* Input field model to enter the token symbol
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
data class TokenSymbol(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* Input field model to enter the token decimals
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
data class Decimals(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* Input field model to enter a custom derivation path
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property showField whether the field should be shown
*/
data class DerivationPath(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val showField: Boolean = false,
) : AddCustomTokenInputField
}
/** Base selector field model of add custom token screen */
internal sealed interface AddCustomTokenSelectorField {
/** Label */
val label: TextReference
/** Selected menu item */
val selectedItem: SelectorItem
/** Menu items */
val items: List<SelectorItem>
/** Lambda be invoked when menu item is been selected */
val onMenuItemClick: (Int) -> Unit
/**
* Network selector model
*
* @property label label
* @property selectedItem selected menu item
* @property items menu items
* @property onMenuItemClick lambda be invoked when menu item is been selected
*/
data class Network(
override val label: TextReference,
override val selectedItem: SelectorItem.Title,
override val items: List<SelectorItem.Title>,
override val onMenuItemClick: (Int) -> Unit,
) : AddCustomTokenSelectorField
/**
* Derivation path selector model
*
* @property label label
* @property selectedItem selected menu item
* @property items menu items
* @property onMenuItemClick lambda be invoked when menu item is been selected
* @property isEnabled selection availability
*/
data class DerivationPath(
override val label: TextReference,
override val selectedItem: SelectorItem.TitleWithSubtitle,
override val items: List<SelectorItem.TitleWithSubtitle>,
override val onMenuItemClick: (Int) -> Unit,
val isEnabled: Boolean,
) : AddCustomTokenSelectorField
/** Base menu item model */
sealed interface SelectorItem {
/** Title */
val title: TextReference
/** Blockchain */
val blockchain: Blockchain
/**
* Menu item with title
*
* @property title title text
* @property blockchain blockchain
*/
data class Title(override val title: TextReference, override val blockchain: Blockchain) : SelectorItem
/**
* Menu item with title ans subtitle
*
* @property title title text
* @property blockchain blockchain
* @property subtitle subtitle text
*/
data class TitleWithSubtitle(
override val title: TextReference,
override val blockchain: Blockchain,
val subtitle: TextReference,
val type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
) : SelectorItem
}
}
enum class DerivationPathSelectorType {
DEFAULT, CUSTOM, BLOCKCHAIN
}
/**
* Warning model of add custom token screen
*
* @property description warning description
*/
internal sealed class AddCustomTokenWarning(val description: TextReference) {
/** Potential scam warning */
data object PotentialScamToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
)
/** Token already added warning */
data object TokenAlreadyAdded : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
)
/** Unsupported token warning */
data class UnsupportedToken(val networkName: String) : AddCustomTokenWarning(
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName),
)
data object WrongDerivationPath : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
)
}
/**
* Floating button of add custom token screen
*
* @property isEnabled button availability
* @property showProgress whether circle progress indication is enabled
* @property onClick lambda be invoked when button is been pressed
*/
internal data class AddCustomTokenFloatingButton(
val isEnabled: Boolean,
val showProgress: Boolean,
val onClick: () -> Unit,
)

View file

@ -1,4 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.models
/** Custom token type */
enum class CustomTokenType { TOKEN, BLOCKCHAIN }

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.models
internal enum class SupportBlockchainType {
SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.routers
import com.tangem.blockchain.common.Blockchain
/**
* Custom token feature router
*
[REDACTED_AUTHOR]
*/
internal interface CustomTokenRouter {
/** Return to last screen */
fun popBackStack()
/** Open wallet (main) screen */
fun openWalletScreen()
/** Open alert if solana network is unsupported
*
* @param blockchain blockchain to show alert
*/
fun openUnsupportedNetworkAlert(blockchain: Blockchain)
fun showGenericErrorAlertAndPopBack()
}

View file

@ -1,41 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.routers
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/** Default implementation of custom token feature router */
internal class DefaultCustomTokenRouter : CustomTokenRouter {
override fun popBackStack() {
store.dispatchNavigationAction(AppRouter::pop)
}
override fun openWalletScreen() {
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
override fun openUnsupportedNetworkAlert(blockchain: Blockchain) {
val alert = AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.alert_manage_tokens_unsupported_curve_message,
args = listOf(blockchain.getNetworkName()),
)
store.dispatchDialogShow(alert)
}
override fun showGenericErrorAlertAndPopBack() {
val alert = AppDialog.SimpleOkDialogRes(
headerId = R.string.common_error,
messageId = R.string.common_unknown_error,
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
)
store.dispatchDialogShow(alert)
}
}

View file

@ -1,91 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.states
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
/**
* State holder of add custom token screen
*
[REDACTED_AUTHOR]
*/
internal sealed interface AddCustomTokenStateHolder {
/** Lambda be invoked when system back action is been called */
val onBackButtonClick: () -> Unit
/** Toolbar model */
val toolbar: AddCustomTokensToolbar
/** Form model */
val form: AddCustomTokenForm
/** Warnings */
val warnings: Set<AddCustomTokenWarning>
/** Floating button model */
val floatingButton: AddCustomTokenFloatingButton
/**
* Util function that allow to make a copy
*
* @param onBackButtonClick lambda be invoked when system back action is been called
* @param toolbar toolbar model
* @param form form model
* @param warnings warnings
* @param floatingButton floating button model
*/
fun copySealed(
onBackButtonClick: () -> Unit = this.onBackButtonClick,
toolbar: AddCustomTokensToolbar = this.toolbar,
form: AddCustomTokenForm = this.form,
warnings: Set<AddCustomTokenWarning> = this.warnings,
floatingButton: AddCustomTokenFloatingButton = this.floatingButton,
): AddCustomTokenStateHolder {
return when (this) {
is Content -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
is TestContent -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
}
}
/**
* Content state
*
* @property onBackButtonClick lambda be invoked when system back action is been called
* @property toolbar toolbar model
* @property form form model
* @property warnings warnings
* @property floatingButton floating button model
*/
data class Content(
override val onBackButtonClick: () -> Unit,
override val toolbar: AddCustomTokensToolbar,
override val form: AddCustomTokenForm,
override val warnings: Set<AddCustomTokenWarning>,
override val floatingButton: AddCustomTokenFloatingButton,
) : AddCustomTokenStateHolder
/**
* Content state with fields for testing
*
* @property onBackButtonClick lambda be invoked when system back action is been called
* @property toolbar toolbar model
* @property form form model
* @property warnings warnings
* @property floatingButton floating button model
* @property testBlock test block model
* @property bottomSheet bottom sheet model
*/
data class TestContent(
override val onBackButtonClick: () -> Unit,
override val toolbar: AddCustomTokensToolbar,
override val form: AddCustomTokenForm,
override val warnings: Set<AddCustomTokenWarning>,
override val floatingButton: AddCustomTokenFloatingButton,
val testBlock: AddCustomTokenTestBlock,
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
) : AddCustomTokenStateHolder
}

View file

@ -1,77 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
/**
* Add custom token content
*
* @param state screen state
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
Scaffold(
modifier = modifier,
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,
onBackButtonClick = state.toolbar.onBackButtonClick,
)
},
floatingActionButton = {
val density = LocalDensity.current
val verticalPadding = TangemTheme.dimens.spacing32
AddCustomTokenFloatingButton(
model = state.floatingButton,
modifier = Modifier.onSizeChanged {
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
},
)
},
floatingActionButtonPosition = FabPosition.Center,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues = it)
.padding(bottom = floatingButtonHeight)
.fillMaxSize(),
) {
AddCustomTokenForm(model = state.form)
AddCustomTokenWarnings(warnings = state.warnings)
}
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenContent() {
TangemThemePreview {
AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent())
}
}

View file

@ -1,126 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.customtoken.impl.presentation.models.*
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
internal object AddCustomTokenPreviewData {
fun createWarnings(): Set<AddCustomTokenWarning> {
return setOf(
AddCustomTokenWarning.PotentialScamToken,
AddCustomTokenWarning.TokenAlreadyAdded,
AddCustomTokenWarning.UnsupportedToken(networkName = "Solana"),
)
}
fun createDefaultForm(): AddCustomTokenForm {
return AddCustomTokenForm(
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_contract_address_input_title),
placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"),
isLoading = false,
isError = false,
error = null,
),
networkSelectorField = AddCustomTokenSelectorField.Network(
label = TextReference.Res(R.string.custom_token_network_input_title),
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
title = TextReference.Res(R.string.custom_token_network_input_not_selected),
blockchain = Blockchain.Unknown,
),
items = emptyList(),
onMenuItemClick = {},
),
tokenNameInputField = AddCustomTokenInputField.TokenName(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_name_input_title),
placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder),
isEnabled = false,
),
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
),
decimalsInputField = AddCustomTokenInputField.Decimals(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_decimals_input_title),
placeholder = TextReference.Str(value = "8"),
isEnabled = false,
),
derivationPathSelectorField = AddCustomTokenSelectorField.DerivationPath(
label = TextReference.Res(R.string.custom_token_derivation_path_input_title),
selectedItem = AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle(
title = TextReference.Res(R.string.custom_token_derivation_path_default),
subtitle = TextReference.Res(R.string.custom_token_derivation_path_default),
blockchain = Blockchain.Unknown,
),
items = emptyList(),
onMenuItemClick = {},
isEnabled = true,
),
derivationPathInputField = null,
)
}
fun createTestContent(): AddCustomTokenStateHolder.TestContent {
return AddCustomTokenStateHolder.TestContent(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = createDefaultForm(),
warnings = createWarnings(),
floatingButton = AddCustomTokenFloatingButton(
isEnabled = false,
showProgress = false,
onClick = {},
),
testBlock = AddCustomTokenTestBlock(
chooseTokenButtonText = "Choose token",
clearButtonText = "Clear address",
resetButtonText = "Reset",
onClearAddressButtonClick = {},
onResetButtonClick = {},
),
bottomSheet = AddCustomTokenChooseTokenBottomSheet(categoriesBlocks = emptyList(), onTestTokenClick = {}),
)
}
fun createContent(): AddCustomTokenStateHolder.Content {
return AddCustomTokenStateHolder.Content(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = createDefaultForm(),
warnings = createWarnings(),
floatingButton = AddCustomTokenFloatingButton(
isEnabled = false,
showProgress = false,
onClick = {},
),
)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
/**
* Add custom token screen
*
* @param stateHolder state holder
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifier: Modifier = Modifier) {
when (stateHolder) {
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(stateHolder, modifier)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(stateHolder, modifier)
}
}
@Preview(showSystemUi = true)
@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AddCustomTokenScreen(
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
) {
TangemThemePreview {
AddCustomTokenScreen(stateHolder)
}
}
private class AddCustomTokenScreenProvider : CollectionPreviewParameterProvider<AddCustomTokenStateHolder>(
collection = listOf(
AddCustomTokenPreviewData.createContent(),
AddCustomTokenPreviewData.createTestContent(),
),
)

View file

@ -1,249 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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.SpacerH8
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Add custom token content for testing
*
* @param state screen state
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
)
BackHandler(
onBack = {
onBackButtonClicked(
coroutineScope = coroutineScope,
bottomSheetScaffoldState = bottomSheetScaffoldState,
defaultAction = state.onBackButtonClick,
)
},
)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
BottomSheetScaffold(
modifier = modifier,
sheetContent = {
SheetContent(
coroutineScope = coroutineScope,
bottomSheetScaffoldState = bottomSheetScaffoldState,
model = state.bottomSheet,
)
},
scaffoldState = bottomSheetScaffoldState,
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,
onBackButtonClick = {
onBackButtonClicked(
coroutineScope,
bottomSheetScaffoldState,
defaultAction = state.toolbar.onBackButtonClick,
)
},
)
},
floatingActionButton = {
val density = LocalDensity.current
val verticalPadding = TangemTheme.dimens.spacing32
AddCustomTokenFloatingButton(
model = state.floatingButton,
modifier = Modifier.onSizeChanged {
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
},
)
},
floatingActionButtonPosition = FabPosition.Center,
sheetBackgroundColor = TangemTheme.colors.background.secondary,
sheetPeekHeight = TangemTheme.dimens.size0,
backgroundColor = TangemTheme.colors.background.primary,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues = it)
.padding(bottom = floatingButtonHeight)
.fillMaxSize(),
) {
TestBlock(
state.testBlock,
coroutineScope,
bottomSheetScaffoldState,
)
AddCustomTokenForm(model = state.form)
AddCustomTokenWarnings(warnings = state.warnings)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
private fun onBackButtonClicked(
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
defaultAction: () -> Unit,
) {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isExpanded) {
bottomSheetScaffoldState.bottomSheetState.collapse()
} else {
defaultAction()
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SheetContent(
model: AddCustomTokenChooseTokenBottomSheet,
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16),
) {
Hand()
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
model.categoriesBlocks.forEachIndexed { index, categoryBlock ->
key(categoryBlock) {
Column {
TokensList(
title = categoryBlock.name,
tokens = categoryBlock.items,
onTestTokenClick = { address ->
model.onTestTokenClick(address)
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() }
},
)
if (model.categoriesBlocks.lastIndex != index) {
Divider()
SpacerH8()
}
}
}
}
}
}
}
@Composable
private fun TokensList(title: String, tokens: List<TestTokenItem>, onTestTokenClick: (String) -> Unit) {
Text(
text = title,
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing24,
vertical = TangemTheme.dimens.spacing8,
),
maxLines = 1,
style = TangemTheme.typography.h3,
)
tokens.forEach { token ->
key(token) {
PrimaryButton(
text = token.name,
onClick = { onTestTokenClick(token.address) },
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
)
}
}
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
@Composable
private fun TestBlock(
model: AddCustomTokenTestBlock,
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
PrimaryButton(
text = model.chooseTokenButtonText,
onClick = {
softwareKeyboardController?.hide()
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.expand() }
},
modifier = Modifier.fillMaxWidth(),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
PrimaryButton(
text = model.clearButtonText,
onClick = model.onClearAddressButtonClick,
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = model.resetButtonText,
onClick = model.onResetButtonClick,
modifier = Modifier.weight(1f),
)
}
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenTestContent() {
TangemThemePreview {
AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent())
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.PrimaryButtonIconStart
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.wallet.R
/**
* Add custom token floating button. Attached above the keyboard.
*
* @param model button model
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
PrimaryButtonIconStart(
modifier = modifier
.imePadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.custom_token_add_token),
iconResId = R.drawable.ic_plus_24,
enabled = model.isEnabled,
showProgress = model.showProgress,
onClick = model.onClick,
)
}
@Preview
@Composable
private fun Preview_AddCustomTokenFloatingButton(
@PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton,
) {
TangemThemePreview {
AddCustomTokenFloatingButton(model)
}
}
private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider<AddCustomTokenFloatingButton>(
listOf(
AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}),
AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}),
),
)

View file

@ -1,212 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.animation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.common.compose.TangemTextFieldsDefault
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
/**
* Add custom token form
*
* @param model component model
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
shape = RoundedCornerShape(TangemTheme.dimens.radius8),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation4,
) {
Column(
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
if (model.showTokenFields) InputField(model = model.contractAddressInputField)
SelectorField(model = model.networkSelectorField)
if (model.showTokenFields) InputField(model = model.tokenNameInputField)
if (model.showTokenFields) InputField(model = model.tokenSymbolInputField)
if (model.showTokenFields) InputField(model = model.decimalsInputField)
model.derivationPathSelectorField?.let { SelectorField(model = it) }
if (model.derivationPathInputField?.showField == true) InputField(model = model.derivationPathInputField)
}
}
}
@Composable
private fun InputField(model: AddCustomTokenInputField) {
Column {
val isError = (model as? AddCustomTokenInputField.ContactAddress)?.isError ?: false
TextField(model, isError)
(model as? AddCustomTokenInputField.ContactAddress)?.error?.resolveReference()?.let {
AnimatedVisibility(
visible = isError,
enter = fadeIn() + slideInVertically(),
exit = slideOutVertically() + fadeOut(),
) {
Text(
text = it,
color = MaterialTheme.colors.error,
style = TangemTheme.typography.body2,
)
}
}
}
}
@Composable
private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
Box {
val isEnabled = when (model) {
is AddCustomTokenInputField.ContactAddress -> true
is AddCustomTokenInputField.Decimals -> model.isEnabled
is AddCustomTokenInputField.TokenName -> model.isEnabled
is AddCustomTokenInputField.TokenSymbol -> model.isEnabled
is AddCustomTokenInputField.DerivationPath -> true
}
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = model.value,
onValueChange = model.onValueChange,
keyboardOptions = model.keyboardOptions,
label = {
Text(
text = model.label.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
enabled = isEnabled,
error = isError,
interactionSource = remember { MutableInteractionSource() },
).value,
)
},
placeholder = {
Text(
text = model.placeholder.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTextFieldsDefault.defaultTextFieldColors
.placeholderColor(enabled = isEnabled)
.value,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
singleLine = true,
enabled = isEnabled,
isError = isError,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
AnimatedVisibility(
visible = (model as? AddCustomTokenInputField.ContactAddress)?.isLoading ?: false,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(horizontal = TangemTheme.dimens.spacing6)
.padding(bottom = TangemTheme.dimens.spacing6),
) {
LinearProgressIndicator(color = TangemTheme.colors.icon.primary1)
}
}
}
/**
* Be careful with this function ExposedDropdownMenuBox can crash the app if it is open and user clicks system back
* button. It was fixed in compose-material 1.6.4.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SelectorField(model: AddCustomTokenSelectorField) {
var isExpanded by remember { mutableStateOf(value = false) }
ExposedDropdownMenuBox(
expanded = isExpanded,
onExpandedChange = { isExpanded = !isExpanded },
) {
val isEnabled = (model as? AddCustomTokenSelectorField.DerivationPath)?.isEnabled ?: true
OutlinedTextField(
value = when (val item = model.selectedItem) {
is AddCustomTokenSelectorField.SelectorItem.Title -> item.title
is AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle -> item.subtitle
}.resolveReference(),
modifier = Modifier.fillMaxWidth(),
onValueChange = {},
readOnly = true,
enabled = isEnabled,
label = { Text(text = model.label.resolveReference()) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
ExposedDropdownMenu(
expanded = isExpanded && isEnabled,
onDismissRequest = { isExpanded = false },
) {
model.items.forEachIndexed { index, item ->
DropdownMenuItem(
onClick = {
model.onMenuItemClick(index)
isExpanded = false
},
) {
Column {
Text(text = item.title.resolveReference())
val subtitle = (item as? AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle)
?.subtitle?.resolveReference()
if (!subtitle.isNullOrBlank()) {
Text(
text = subtitle,
color = TangemTheme.colors.text.secondary,
maxLines = 1,
style = TangemTheme.typography.caption2,
)
}
}
}
}
}
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) {
TangemThemePreview {
AddCustomTokenForm(model)
}
}
private class AddCustomTokenFormProvider : CollectionPreviewParameterProvider<AddCustomTokenForm>(
collection = listOf(
AddCustomTokenPreviewData.createDefaultForm(),
AddCustomTokenPreviewData.createDefaultForm().copy(derivationPathSelectorField = null),
AddCustomTokenPreviewData.createDefaultForm().let { form ->
form.copy(contractAddressInputField = form.contractAddressInputField.copy(isLoading = true))
},
),
)

View file

@ -1,61 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
/**
* Add custom token toolbar
*
* @param title title
* @param onBackButtonClick lambda be invoked when BackButton is been pressed
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) {
val toolbarElevation = if (isSystemInDarkTheme()) {
TangemTheme.dimens.elevation0
} else {
AppBarDefaults.TopAppBarElevation
}
TopAppBar(
backgroundColor = TangemTheme.colors.background.primary,
elevation = toolbarElevation,
) {
IconButton(onClick = onBackButtonClick) {
Icon(
painter = painterResource(id = R.drawable.ic_back_24),
contentDescription = null,
tint = TangemTheme.colors.icon.secondary,
)
}
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing26))
Text(
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.h3,
)
}
}
@Preview
@Composable
internal fun Preview_AddCustomTokenToolbar() {
TangemThemePreview {
AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {})
}
}

View file

@ -1,82 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
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.customtoken.impl.presentation.models.AddCustomTokenWarning
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.wallet.R
/**
* Add custom token warnings
*
* @param warnings warnings descriptions set
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenWarnings(warnings: Set<AddCustomTokenWarning>) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
warnings.forEach { warning ->
key(warning) { AddCustomTokenWarning(warning) }
}
}
}
@Composable
private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) {
Card(
modifier = Modifier.fillMaxSize(),
shape = TangemTheme.shapes.roundedCornersSmall2,
backgroundColor = TangemColorPalette.Tangerine,
contentColor = TangemColorPalette.White,
elevation = TangemTheme.dimens.elevation4,
) {
// FIXME("Incorrect typography. Replace with typography from design system")
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = stringResource(id = R.string.common_warning),
maxLines = 1,
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
)
Text(
text = warning.description.resolveReference(),
fontSize = 13.sp,
lineHeight = 18.sp,
)
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AddCustomTokenWarnings() {
TangemThemePreview {
AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings())
}
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressService
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Validator of contract address
*
[REDACTED_AUTHOR]
*/
object ContractAddressValidator {
/** Validate a [address] using [blockchain] */
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
return when {
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY)
validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
}
}
private fun validateAddress(blockchain: Blockchain, address: String): Boolean {
return when (blockchain) {
Blockchain.Unknown,
Blockchain.Binance,
Blockchain.BinanceTestnet,
-> SuccessAddressValidator.validate(address)
Blockchain.Cardano -> blockchain.validateContractAddress(address)
else -> blockchain.validateAddress(address)
}
}
private object SuccessAddressValidator : AddressService() {
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
throw UnsupportedOperationException()
}
override fun validate(address: String): Boolean = true
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Result of validation contract address
*
[REDACTED_AUTHOR]
*/
sealed interface ContractAddressValidatorResult {
/** Success */
object Success : ContractAddressValidatorResult
/**
* Error
*
* @property type type of error
*/
data class Error(val type: AddCustomTokenError) : ContractAddressValidatorResult
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.analytics.events.ManageTokens
/** Analytics sender for tokens list screen */
class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
fun sendWhenScreenOpened() {
analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened)
}
fun sendWhenAddTokenButtonClicked(customCurrency: CustomCurrency) {
analyticsEventHandler.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
}
}

View file

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

View file

@ -2,36 +2,24 @@ package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult 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.doOnFailure
import com.tangem.common.doOnSuccess import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute 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.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.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse 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.*
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject 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.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -41,7 +29,6 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action import org.rekotlin.Action
import org.rekotlin.Middleware import org.rekotlin.Middleware
import timber.log.Timber import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware { class DetailsMiddleware {
private val appSettingsMiddleware = AppSettingsMiddleware() private val appSettingsMiddleware = AppSettingsMiddleware()
@ -62,7 +49,6 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) { private fun handleAction(state: DetailsState, action: Action) {
when (action) { when (action) {
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, 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 package com.tangem.tap.features.details.redux
import com.tangem.domain.apptheme.model.AppThemeMode 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.extensions.inject
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
@ -21,7 +20,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
val detailsState = state.detailsState val detailsState = state.detailsState
return when (action) { return when (action) {
is DetailsAction.PrepareScreen -> { is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state) handlePrepareScreen(action)
} }
is DetailsAction.AppSettings -> { is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState) handlePrivacyAction(action, detailsState)
@ -31,26 +30,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency, 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( return DetailsState(
scanResponse = action.scanResponse, scanResponse = action.scanResponse,
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState( appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking { isBiometricsAvailable = runBlocking {
tangemSdkManager.checkCanUseBiometry() tangemSdkManager.checkCanUseBiometry()

View file

@ -1,27 +1,16 @@
package com.tangem.tap.features.details.redux 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.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.StateType import org.rekotlin.StateType
data class DetailsState( data class DetailsState(
@Deprecated("Delete after onboarding refactoring")
val scanResponse: ScanResponse? = null, val scanResponse: ScanResponse? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
val error: TextReference? = null,
val appSettingsState: AppSettingsState = AppSettingsState(), val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType ) : StateType
sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}
data class AppSettingsState( data class AppSettingsState(
val saveWallets: Boolean = false, val saveWallets: Boolean = false,
val saveAccessCodes: 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.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository 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.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -45,7 +44,6 @@ internal class AppSettingsViewModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository, private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val detailsFeatureToggles: DetailsFeatureToggles,
) : ViewModel(), ) : ViewModel(),
StoreSubscriber<DetailsState>, StoreSubscriber<DetailsState>,
DefaultLifecycleObserver { DefaultLifecycleObserver {
@ -62,9 +60,7 @@ internal class AppSettingsViewModel @Inject constructor(
init { init {
bootstrapAppCurrencyUpdates() bootstrapAppCurrencyUpdates()
if (detailsFeatureToggles.isRedesignEnabled) { bootstrapBiometricsUpdates()
bootstrapBiometricsUpdates()
}
subscribeToStoreChanges() subscribeToStoreChanges()
sendItemsAnalytics() 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.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected 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.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.store import com.tangem.tap.store

View file

@ -24,12 +24,6 @@ abstract class BaseDisclaimer(
override suspend fun isAccepted(): Boolean = dataProvider.isAccepted() 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) { class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html") 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( StoriesScreenContent(
modifier = Modifier.fillMaxSize().testTag(TestTags.STORIES_SCREEN), modifier = Modifier
.fillMaxSize()
.testTag(TestTags.STORIES_SCREEN),
config = StoriesScreenContentConfig( config = StoriesScreenContentConfig(
storiesSize = state.stories.lastIndex, storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex, currentStoryIndex = currentStoryIndex,
@ -157,7 +159,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
duration = currentStoryDuration, duration = currentStoryDuration,
) )
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet() Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
is Stories.UltraSecureBackup -> StoriesUltraSecureBackup( Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
isPaused = isPaused, isPaused = isPaused,
stepDuration = currentStoryDuration, 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 ReadCard(val scope: CoroutineScope) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : 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 { private fun internalReduce(action: Action, appState: AppState): HomeState {
if (action !is HomeAction) return appState.homeState if (action !is HomeAction) return appState.homeState
var state = appState.homeState return when (action) {
when (action) {
is HomeAction.ScanInProgress -> { is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress) appState.homeState.copy(scanInProgress = action.scanInProgress)
} }
is HomeAction.UpdateCountryCode -> { else -> appState.homeState
state.onCountryCodeUpdate(state, action.userCountryCode)
}
else -> {}
} }
return state
} }

View file

@ -1,56 +1,24 @@
package com.tangem.tap.features.home.redux package com.tangem.tap.features.home.redux
import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.MutableState import kotlinx.collections.immutable.toImmutableList
import androidx.compose.runtime.mutableStateOf
import org.rekotlin.StateType import org.rekotlin.StateType
import java.util.Locale
@Immutable
data class HomeState( data class HomeState(
val scanInProgress: Boolean = false, val scanInProgress: Boolean = false,
val stories: List<Stories> = initDefaultStories(), val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),
) : StateType { ) : StateType {
val firstStory: Stories val firstStory: Stories get() = stories[0]
get() = stories[0]
fun stepOf(story: Stories): Int = stories.indexOf(story) 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( enum class Stories(val duration: Int = 6000) {
val duration: Int, TangemIntro,
val isNewWalletAvailable: MutableState<Boolean> = mutableStateOf(HomeState.isNewWalletAvailableInit()), RevolutionaryWallet,
) { UltraSecureBackup,
object TangemIntro : Stories(duration = 6000) Currencies,
object RevolutionaryWallet : Stories(duration = 6000) Web3,
object UltraSecureBackup : Stories(duration = 6000) WalletForEveryone,
object Currencies : Stories(duration = 6000)
object Web3 : Stories(duration = 6000)
object WalletForEveryone : Stories(duration = 6000)
} }

View file

@ -13,16 +13,15 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext 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.features.main.model.MainScreenState
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -41,12 +40,12 @@ internal class MainViewModel @Inject constructor(
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
private val blockchainSDKFactory: BlockchainSDKFactory, private val blockchainSDKFactory: BlockchainSDKFactory,
private val userWalletsListManager: UserWalletsListManager, private val userWalletsListManager: UserWalletsListManager,
private val walletManagersFacade: WalletManagersFacade,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggles: StakingFeatureToggles, stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager, private val apiConfigsManager: ApiConfigsManager,
homeFeatureToggles: HomeFeatureToggles,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents { ) : ViewModel(), MainIntents {
@ -67,6 +66,14 @@ internal class MainViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
viewModelScope.launch {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
}
}
updateAppCurrencies() updateAppCurrencies()
observeFlips() observeFlips()
displayBalancesHidingStatusToast() displayBalancesHidingStatusToast()
@ -97,19 +104,6 @@ internal class MainViewModel @Inject constructor(
.onEach { userWallet -> .onEach { userWallet ->
Analytics.setContext(userWallet.scanResponse) Analytics.setContext(userWallet.scanResponse)
Analytics.send(Basic.WalletOpened()) Analytics.send(Basic.WalletOpened())
if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) {
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
infoHolder.setCardInfo(userWallet.scanResponse)
walletManagersFacade
.getAll(userWallet.walletId)
.distinctUntilChanged()
.onEach(infoHolder::setWalletsInfo)
.catch { Timber.e(it) }
.launchIn(viewModelScope)
}
}
} }
.flowOn(dispatchers.io) .flowOn(dispatchers.io)
.launchIn(viewModelScope) .launchIn(viewModelScope)

View file

@ -1,22 +1,31 @@
package com.tangem.tap.features.onboarding package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic 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.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse 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.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter 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.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.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState 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( private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse, scanResponse: ScanResponse,
backupCardsIds: List<String>?, backupCardsIds: List<String>?,

Some files were not shown because too many files have changed in this diff Show more