Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-14 20:53:14 +04:00
commit 1f6cbc7ed0
537 changed files with 6377 additions and 12897 deletions

View file

@ -9,8 +9,8 @@ import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
@ -18,24 +18,23 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
@ -47,7 +46,7 @@ import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@Suppress("TooManyFunctions")
interface ApplicationEntryPoint {
fun getConfigManager(): ConfigManager
fun getEnvironmentConfigStorage(): EnvironmentConfigStorage
fun getAppStateHolder(): AppStateHolder
@ -57,7 +56,7 @@ interface ApplicationEntryPoint {
fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCustomTokenFeatureToggles(): CustomTokenFeatureToggles
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository
@ -95,20 +94,16 @@ interface ApplicationEntryPoint {
fun getCardRepository(): CardRepository
fun getFeedbackManagerFeatureToggles(): FeedbackManagerFeatureToggles
fun getTangemSdkLogger(): TangemSdkLogger
fun getSettingsRepository(): SettingsRepository
fun getBlockchainSDKFactory(): BlockchainSDKFactory
fun getGetFeedbackEmailUseCase(): GetFeedbackEmailUseCase
fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase
fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener
@ -117,9 +112,11 @@ interface ApplicationEntryPoint {
fun getAppRouter(): AppRouter
fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles
fun getTangemAppLogger(): TangemAppLoggerInitializer
fun getTransactionSignerFactory(): TransactionSignerFactory
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.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -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.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
@ -550,11 +548,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
} else {
lifecycleScope.launch {
val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
val isPushPermissionEnabled = toggles.isPushNotificationsEnabled
val shouldShowTos = !cardRepository.isTangemTOSAccepted() && isPushPermissionEnabled
val wasPushInitiallyAsked = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val shouldShowInitialPush = wasPushInitiallyAsked && isPushPermissionEnabled
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val shouldShowInitialPush = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)

View file

@ -1,13 +1,10 @@
package com.tangem.tap
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
@ -24,10 +21,9 @@ import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
@ -36,36 +32,32 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
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.log.TangemAppLoggerInitializer
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
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.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -90,8 +82,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val appStateHolder: AppStateHolder
get() = entryPoint.getAppStateHolder()
private val configManager: ConfigManager
get() = entryPoint.getConfigManager()
private val environmentConfigStorage: EnvironmentConfigStorage
get() = entryPoint.getEnvironmentConfigStorage()
private val assetLoader: AssetLoader
get() = entryPoint.getAssetLoader()
@ -102,8 +94,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager()
private val customTokenFeatureToggles: CustomTokenFeatureToggles
get() = entryPoint.getCustomTokenFeatureToggles()
private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCardScanningFeatureToggles()
private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository()
@ -159,9 +151,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val cardRepository: CardRepository
get() = entryPoint.getCardRepository()
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
get() = entryPoint.getFeedbackManagerFeatureToggles()
private val tangemSdkLogger: TangemSdkLogger
get() = entryPoint.getTangemSdkLogger()
@ -171,8 +160,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val blockchainSDKFactory: BlockchainSDKFactory
get() = entryPoint.getBlockchainSDKFactory()
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase
get() = entryPoint.getGetFeedbackEmailUseCase()
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getSendFeedbackEmailUseCase()
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase()
@ -180,9 +169,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles()
private val urlOpener
get() = entryPoint.getUrlOpener()
@ -192,14 +178,17 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val appRouter: AppRouter
get() = entryPoint.getAppRouter()
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles
get() = entryPoint.getPushNotificationsFeatureToggles()
private val tangemAppLoggerInitializer: TangemAppLoggerInitializer
get() = entryPoint.getTangemAppLogger()
private val transactionSignerFactory: TransactionSignerFactory
get() = entryPoint.getTransactionSignerFactory()
private val homeFeatureToggles: HomeFeatureToggles
get() = entryPoint.getHomeFeatureToggles()
private val getUserCountryUseCase: GetUserCountryUseCase
get() = entryPoint.getGetUserCountryCodeUseCase()
// endregion
override fun onCreate() {
@ -221,10 +210,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
initConfigManager(
loader = FeaturesLocalLoader(assetLoader, BuildConfig.ENVIRONMENT),
onComplete = ::initWithConfigDependency,
)
val config = environmentConfigStorage.initialize()
store.dispatch(GlobalAction.SetConfigManager(environmentConfigStorage))
initWithConfigDependency(environmentConfig = config)
}
loadNativeLibraries()
@ -242,7 +232,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
)
appStateHolder.mainStore = store
walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId)
walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId)
}
private fun createReduxStore(): Store<AppState> {
@ -252,7 +242,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
state = AppState(
daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager,
customTokenFeatureToggles = customTokenFeatureToggles,
cardScanningFeatureToggles = cardScanningFeatureToggles,
walletConnectRepository = walletConnect2Repository,
walletConnectSessionsRepository = walletConnectSessionsRepository,
scanCardProcessor = scanCardProcessor,
@ -269,20 +259,19 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
generateWalletNameUseCase = generateWalletNameUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
tangemSdkLogger = tangemSdkLogger,
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener,
shareManager = shareManager,
appRouter = appRouter,
pushNotificationsFeatureToggles = pushNotificationsFeatureToggles,
transactionSignerFactory = transactionSignerFactory,
homeFeatureToggles = homeFeatureToggles,
getUserCountryUseCase = getUserCountryUseCase,
),
),
)
@ -299,19 +288,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
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(environmentConfig: EnvironmentConfig) {
initAnalytics(this, environmentConfig)
Log.addLogger(logger = tangemSdkLogger)
}
private fun initWithConfigDependency(config: Config) {
initAnalytics(this, config)
initFeedbackManager(this, store)
}
private fun initAnalytics(application: Application, config: Config) {
private fun initAnalytics(application: Application, environmentConfig: EnvironmentConfig) {
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
@ -320,7 +302,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
val buildData = AnalyticsHandlerBuilder.Data(
application = application,
config = config,
config = environmentConfig,
isDebug = BuildConfig.DEBUG,
logConfig = LogConfig.analyticsHandlers,
jsonConverter = MoshiConverter.sdkMoshiConverter,
@ -347,51 +329,4 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
factory.build(Analytics, buildData)
// 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

@ -3,15 +3,15 @@ package com.tangem.tap.common.analytics.api
import android.app.Application
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.common.AnalyticsHandlersLogConfig
import com.tangem.datasource.config.models.Config
interface AnalyticsHandlerBuilder {
fun build(data: Data): AnalyticsHandler?
data class Data(
val application: Application,
val config: Config,
val config: EnvironmentConfig,
val isDebug: Boolean,
val logConfig: AnalyticsHandlersLogConfig,
val jsonConverter: MoshiJsonConverter,

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

View file

@ -20,8 +20,4 @@ fun BigDecimal.toFormattedString(
df.isGroupingUsed = true
df.roundingMode = roundingMode
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.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
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.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer
@ -28,8 +26,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
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.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
@ -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.saveWallet.redux.SaveWalletMiddleware
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.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState
@ -43,8 +40,6 @@ data class AppState(
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
@ -75,7 +70,6 @@ data class AppState(
OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,

View file

@ -2,16 +2,15 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
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.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -31,7 +30,11 @@ sealed class GlobalAction : Action {
* For resuming unfinished backup of standard Wallet see
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
data class Start(
val scanResponse: ScanResponse,
val source: BackupStartedSource,
val canSkipBackup: Boolean = true,
) : Onboarding()
/**
* Initiate resuming of unfinished backup for standard Wallet.
@ -71,10 +74,7 @@ sealed class GlobalAction : Action {
data class IsSignWithRing(val isSignWithRing: Boolean) : 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()
data class SetConfigManager(val environmentConfigStorage: EnvironmentConfigStorage) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : GlobalAction() {

View file

@ -4,7 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.CardDTO
@ -55,15 +55,9 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
is GlobalAction.RestoreAppCurrency -> {
restoreAppCurrency()
}
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = action.feedbackData,
scanResponse = action.scanResponse,
)
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config ?: return
val config = appStateSafe.globalState.environmentConfigStorage?.getConfigSync() ?: return
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
@ -92,21 +86,24 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
scope.launch { exchangeManager.update() }
}
is GlobalAction.FetchUserCountry -> {
scope.launch {
// TODO("After adding DI") get dependencies by DI
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
.onSuccess {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
)
}
.onFailure {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
scope.launch {
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
.onSuccess {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
)
}
.onFailure {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
}
}
}
}
@ -143,21 +140,24 @@ private fun restoreAppCurrency() {
}
}
private fun makeSellExchangeService(config: Config): ExchangeService {
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return MoonPayService(
apiKey = config.moonPayApiKey,
secretKey = config.moonPayApiSecretKey,
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
)
}
private fun makeBuyExchangeService(config: Config): ExchangeService {
private fun makeBuyExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return BuyExchangeService(
mercuryoService = makeMercuryoExchangeService(config),
mercuryoService = makeMercuryoExchangeService(environmentConfig),
)
}
private fun makeMercuryoExchangeService(config: Config): MercuryoService {
val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret)
private fun makeMercuryoExchangeService(environmentConfig: EnvironmentConfig): MercuryoService {
val mercuryoEnvironment = MercuryoEnvironment.prod(
environmentConfig.mercuryoWidgetId,
environmentConfig.mercuryoSecret,
)
return MercuryoService(mercuryoEnvironment)
}

View file

@ -2,12 +2,9 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -51,7 +48,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.SetConfigManager -> {
globalState.copy(configManager = action.configManager)
globalState.copy(environmentConfigStorage = action.environmentConfigStorage)
}
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanResponse?.card ?: return globalState
@ -72,9 +69,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}
is GlobalAction.ShowDialog -> {
globalState.copy(dialog = action.stateDialog)
}
@ -87,10 +81,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> {
store.dispatchOnMain(HomeAction.UpdateCountryCode(action.countryCode))
globalState.copy(
userCountryCode = action.countryCode,
)
globalState.copy(userCountryCode = action.countryCode)
}
else -> globalState
}

View file

@ -1,11 +1,10 @@
package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -17,12 +16,12 @@ data class GlobalState(
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val configManager: ConfigManager? = null,
val feedbackManager: LegacyFeedbackManager? = null,
val environmentConfigStorage: EnvironmentConfigStorage? = null,
val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
@Deprecated("Use GetUserCountryCodeUseCase instead")
val userCountryCode: String? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
val isLastSignWithRing: Boolean = false,

View file

@ -1,18 +1,12 @@
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.utils.convertToSdkAmount
import com.tangem.tap.common.extensions.dispatchWithMain
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.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -23,7 +17,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
internal object LegacyMiddleware {
@ -33,68 +26,14 @@ internal object LegacyMiddleware {
{ next ->
{ 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 -> {
store.dispatch(
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup),
)
}
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),
GlobalAction.Onboarding.Start(
scanResponse = action.scanResponse,
)
} 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,
)
}
}
source = BackupStartedSource.CreateBackup,
canSkipBackup = action.canSkipBackup,
),
)
}
is LegacyAction.PrepareDetailsScreen -> {
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.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.home.LocaleRegionProvider
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.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
@ -65,7 +68,11 @@ internal object ScanFailsDialog {
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
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 {
store.dispatchDialogHide()

View file

@ -0,0 +1,72 @@
package com.tangem.tap.core.navigation.email
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.foregroundActivityObserver
import timber.log.Timber
/**
* Implementation of email sender for Android
*
[REDACTED_AUTHOR]
*/
internal class AndroidEmailSender : EmailSender {
override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) {
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 packageManager = activity.packageManager
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(activity, email).apply { setPackage(it.activityInfo.packageName) }
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...").apply {
putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
}
ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: Exception) {
Timber.e("Failed to send email: $ex")
}
}
private fun createEmailShareIntent(activity: AppCompatActivity, email: EmailSender.Email): Intent {
val builder = ShareCompat.IntentBuilder(activity)
.setType("message/rfc822")
.setEmailTo(arrayOf(email.address))
.setSubject(email.subject)
.setText(email.message)
email.attachment?.let {
builder.setStream(
FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it),
)
}
return builder.intent
}
}

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.share.ShareManager
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.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager
@ -30,10 +28,6 @@ internal object UtilsModule {
@Singleton
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
@Provides
@Singleton
fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager()
@Provides
@Singleton
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)
internal object CardDomainModule {
@Provides
@Singleton
fun provideGetBiometricsStatusUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): GetBiometricsStatusUseCase {
return GetBiometricsStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideSetAccessCodeRequestPolicyUseCase(
@ -38,12 +30,6 @@ internal object CardDomainModule {
return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
return WasCardScannedUseCase(cardRepository = cardRepository)
}
@Provides
@Singleton
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {

View file

@ -1,8 +1,10 @@
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.usecase.GenerateWalletNameUseCase
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import dagger.Module
import dagger.Provides
@ -14,6 +16,12 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object CardLegacyDomainModule {
@Provides
@Singleton
fun provideCardScanningFeatureToggles(featureTogglesManager: FeatureTogglesManager): CardScanningFeatureToggles {
return CardScanningFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor()

View file

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

View file

@ -63,4 +63,10 @@ object MarketsDomainModule {
networksRepository = networksRepository,
)
}
@Provides
@Singleton
fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase {
return GetTokenExchangesUseCase(marketsTokenRepository = marketsTokenRepository)
}
}

View file

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

View file

@ -206,4 +206,10 @@ internal object StakingDomainModule {
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase {
return GetStakingIntegrationIdUseCase(stakingRepository)
}
}

View file

@ -287,12 +287,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
@Provides
@Singleton
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
@Singleton
fun provideGetBalanceNotEnoughForFeeWarningUseCase(

View file

@ -42,21 +42,6 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideSendMultipleTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
): SendMultipleTransactionUseCase {
return SendMultipleTransactionUseCase(
demoConfig = DemoConfig(),
cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideAssociateAssetUseCase(

View file

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

View file

@ -12,8 +12,8 @@ import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.BackendId
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -21,6 +21,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -47,7 +48,7 @@ internal class DefaultDerivationsRepository(
getNetwork(
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
extraDerivationPath = null,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
)
},
)
@ -73,17 +74,17 @@ internal class DefaultDerivationsRepository(
override suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<Network.ID, String?>,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) ->
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
getNetwork(
blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null,
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
)
},
)
@ -92,15 +93,20 @@ internal class DefaultDerivationsRepository(
}
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
return response.entries
}
.doOnFailure { throw it }
}
// todo replace it in task [REDACTED_JIRA]
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId)
tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
preflightReadFilter = preflightReadFilter,
).doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
return response.entries
}
.doOnFailure { throw it }
}
.doOnFailure { throw it }
error("This code should never be reached")

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
internal class DefaultScanCardProcessor : ScanCardProcessor {
private val isNewCardScanningEnabled: Boolean
get() = store.inject(DaggerGraphState::customTokenFeatureToggles).isNewCardScanningEnabled
get() = store.inject(DaggerGraphState::cardScanningFeatureToggles).isNewCardScanningEnabled
override suspend fun scan(
cardId: String?,

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
@ -18,12 +17,10 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -102,6 +99,8 @@ internal object LegacyScanProcessor {
}
}
// TODO: [REDACTED_JIRA]
@Suppress("UnusedPrivateMember")
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
@ -109,7 +108,6 @@ internal object LegacyScanProcessor {
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {
nextHandler(scanResponse)
@ -120,23 +118,9 @@ internal object LegacyScanProcessor {
withContext(Dispatchers.Main.immediate) {
disclaimerWillShow()
store.dispatch(
DisclaimerAction.Show(
from = DisclaimerSource.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main.immediate) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main.immediate) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
)
store.dispatchNavigationAction {
push(AppRoute.Disclaimer(isTosAccepted = false))
}
}
}
}
@ -149,10 +133,6 @@ internal object LegacyScanProcessor {
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
@ -162,6 +142,7 @@ internal object LegacyScanProcessor {
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = scanResponse,
source = BackupStartedSource.Onboarding,
canSkipBackup = scanResponse.card.canSkipBackup,
),
)

View file

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

View file

@ -16,10 +16,10 @@ import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
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.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
@ -32,17 +32,14 @@ import org.rekotlin.Store
* - [ScanChainException.OnboardingNeeded] if onboarding required.
*
* @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.
*/
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when {
@ -52,6 +49,7 @@ class CheckForOnboardingChain(
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = previousChainResult,
source = BackupStartedSource.Onboarding,
canSkipBackup = previousChainResult.card.canSkipBackup,
),
)

View file

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

View file

@ -17,6 +17,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -59,6 +60,7 @@ interface TangemSdkManager {
suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
preflightReadFilter: PreflightReadFilter?,
): CompletionResult<DerivationTaskResponse>
suspend fun deriveExtendedPublicKey(
@ -99,6 +101,7 @@ interface TangemSdkManager {
)
suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
preflightReadFilter: PreflightReadFilter?,
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,

View file

@ -29,6 +29,7 @@ import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.derivationsFinder
@ -157,6 +158,7 @@ class DefaultTangemSdkManager(
Message(resources.getString(R.string.initial_message_create_wallet_body))
},
iconScanRes = if (scanResponse.cardTypesResolver.isRing()) R.drawable.img_hand_scan_ring else null,
preflightReadFilter = null,
)
.doOnResult { tangemSdk.config.setupForProduct(ProductType.ANY) }
}
@ -195,6 +197,7 @@ class DefaultTangemSdkManager(
} else {
Message(resources.getString(R.string.initial_message_create_wallet_body))
},
preflightReadFilter = null,
)
.doOnResult { tangemSdk.config.setupForProduct(ProductType.ANY) }
}
@ -210,8 +213,13 @@ class DefaultTangemSdkManager(
override suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
preflightReadFilter: PreflightReadFilter?,
): CompletionResult<DerivationTaskResponse> {
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
return runTaskAsyncReturnOnMain(
runnable = DeriveMultipleWalletPublicKeysTask(derivations),
cardId = cardId,
preflightReadFilter = preflightReadFilter,
)
}
override suspend fun deriveExtendedPublicKey(
@ -317,13 +325,21 @@ class DefaultTangemSdkManager(
override suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
preflightReadFilter: PreflightReadFilter?,
cardId: String?,
initialMessage: Message?,
accessCode: String?,
@DrawableRes iconScanRes: Int?,
): CompletionResult<T> = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result ->
tangemSdk.startSessionWithRunnable(
runnable = runnable,
cardId = cardId,
initialMessage = initialMessage,
accessCode = accessCode,
preflightReadFilter = preflightReadFilter,
iconScanRes = iconScanRes,
) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
@ -333,8 +349,14 @@ class DefaultTangemSdkManager(
runnable: CardSessionRunnable<T>,
cardId: String? = null,
initialMessage: Message? = null,
preflightReadFilter: PreflightReadFilter? = null,
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
val result = runTaskAsync(
runnable = runnable,
cardId = cardId,
initialMessage = initialMessage,
preflightReadFilter = preflightReadFilter,
)
return withContext(Dispatchers.Main) { result }
}
@ -386,6 +408,7 @@ class DefaultTangemSdkManager(
runnable = CreateFirstTwinWalletTask(cardId),
cardId = cardId,
initialMessage = initialMessage,
preflightReadFilter = null,
)
}
@ -404,7 +427,7 @@ class DefaultTangemSdkManager(
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
)
return runTaskAsync(task, null, initialMessage)
return runTaskAsync(runnable = task, cardId = null, initialMessage = initialMessage, preflightReadFilter = null)
}
override fun changeProductType(isRing: Boolean) {
@ -427,6 +450,7 @@ class DefaultTangemSdkManager(
runnable = FinalizeTwinTask(secondCardPublicKey, issuerKeyPair),
cardId = cardId,
initialMessage = initialMessage,
preflightReadFilter = null,
)
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.sdk.mocks.MockProvider
@ -72,6 +73,7 @@ class MockTangemSdkManager(
override suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
preflightReadFilter: PreflightReadFilter?,
): CompletionResult<DerivationTaskResponse> {
return MockProvider.getDerivationTaskResponse()
}
@ -135,6 +137,7 @@ class MockTangemSdkManager(
override suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
preflightReadFilter: PreflightReadFilter?,
cardId: String?,
initialMessage: Message?,
accessCode: String?,

View file

@ -17,6 +17,7 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*
[REDACTED_AUTHOR]
*/
// TODO remove it after test after resolve [REDACTED_JIRA]
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
) : CardSessionRunnable<Boolean> {

View file

@ -4,12 +4,12 @@ import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.GsonBuilder
import com.tangem.blockchain.common.Blockchain
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.WcBinanceTransferOrder
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.TradeData
import com.tangem.utils.extensions.stripZeroPlainString
import timber.log.Timber
internal object BnbHelper {

View file

@ -241,16 +241,22 @@ class WalletConnectSdkHelper {
private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? {
val dataToSign = EthereumUtils.buildTransactionToSign(
transactionData = data.transaction,
nonce = null,
blockchain = data.walletManager.wallet.blockchain,
) ?: return null
)
val command = SignHashCommand(
hash = dataToSign.hash,
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
@ -293,7 +299,14 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
@ -387,7 +400,13 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
@ -422,7 +441,13 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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