Updated on 2026-08-14
This commit is contained in:
commit
a56bcff7dd
438 changed files with 9017 additions and 4452 deletions
|
|
@ -134,7 +134,6 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.core.error.ext)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.auth)
|
||||
|
|
@ -280,6 +279,8 @@ dependencies {
|
|||
implementation(tangemDeps.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(tangemDeps.hot.android)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
|
|
@ -138,8 +137,6 @@ interface ApplicationEntryPoint {
|
|||
|
||||
fun getWorkerFactory(): HiltWorkerFactory
|
||||
|
||||
fun getOnlineCardVerifier(): OnlineCardVerifier
|
||||
|
||||
fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory
|
||||
|
||||
fun getApiConfigsManager(): ApiConfigsManager
|
||||
|
|
|
|||
|
|
@ -26,17 +26,13 @@ import androidx.lifecycle.flowWithLifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.RoutingFeatureToggle
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.WEBLINK_KEY
|
||||
import com.tangem.core.navigation.email.EmailSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
|
||||
import com.tangem.data.card.sdk.CardSdkOwner
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
|
@ -53,6 +49,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.features.tester.api.TesterMenuLauncher
|
||||
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
|
||||
import com.tangem.google.GoogleServicesHelper
|
||||
import com.tangem.operations.backup.BackupService
|
||||
|
|
@ -69,7 +66,6 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle
|
|||
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import com.tangem.tap.features.main.MainViewModel
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
|
|
@ -103,9 +99,6 @@ val mainScope = CoroutineScope(mainCoroutineContext)
|
|||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||
|
||||
@Inject
|
||||
lateinit var appStateHolder: AppStateHolder
|
||||
|
||||
/** Router for opening tester menu */
|
||||
@Inject
|
||||
lateinit var cardSdkOwner: CardSdkOwner
|
||||
|
|
@ -122,9 +115,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
@Inject
|
||||
lateinit var walletConnectInteractor: WalletConnectInteractor
|
||||
|
||||
@Inject
|
||||
lateinit var deepLinksRegistry: DeepLinksRegistry
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
|
|
@ -143,9 +133,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
@Inject
|
||||
lateinit var userWalletsListManager: UserWalletsListManager
|
||||
|
||||
@Inject
|
||||
lateinit var emailSender: EmailSender
|
||||
|
||||
@Inject
|
||||
@RootAppComponentContext
|
||||
internal lateinit var rootComponentContext: AppComponentContext
|
||||
|
|
@ -174,15 +161,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
@Inject
|
||||
lateinit var dispatchers: CoroutineDispatcherProvider
|
||||
|
||||
@Inject
|
||||
internal lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector
|
||||
|
||||
@Inject
|
||||
internal lateinit var routingFeatureToggle: RoutingFeatureToggle
|
||||
|
||||
@Inject
|
||||
internal lateinit var deeplinkFactory: DeepLinkFactory
|
||||
|
||||
|
|
@ -192,6 +173,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
@Inject
|
||||
internal lateinit var urlOpener: UrlOpener
|
||||
|
||||
@Inject
|
||||
internal lateinit var testerMenuLauncher: TesterMenuLauncher
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
|
@ -244,13 +228,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
sendStakingUnsubmittedHashes()
|
||||
checkGoogleServicesAvailability()
|
||||
|
||||
if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) {
|
||||
// handle intent only on start, not on recreate
|
||||
handleDeepLink(intent = intent, isFromOnNewIntent = false)
|
||||
}
|
||||
|
||||
lifecycle.addObserver(WindowObscurationObserver)
|
||||
lifecycle.addObserver(defaultDeviceFlipDetector)
|
||||
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRootContent() {
|
||||
|
|
@ -481,7 +464,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
}
|
||||
|
||||
if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) {
|
||||
if (intent != null) {
|
||||
handleDeepLink(intent = intent, isFromOnNewIntent = false)
|
||||
}
|
||||
|
||||
|
|
@ -489,26 +472,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) {
|
||||
if (routingFeatureToggle.isDeepLinkNavigationEnabled) {
|
||||
val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri()
|
||||
val webLink = intent.getStringExtra(WEBLINK_KEY)
|
||||
val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri()
|
||||
val webLink = intent.getStringExtra(WEBLINK_KEY)
|
||||
|
||||
val receivedDeepLink = intent.data ?: deepLinkExtras
|
||||
val receivedDeepLink = intent.data ?: deepLinkExtras
|
||||
|
||||
when {
|
||||
receivedDeepLink != null -> {
|
||||
deeplinkFactory.handleDeeplink(
|
||||
deeplinkUri = receivedDeepLink,
|
||||
coroutineScope = lifecycleScope,
|
||||
isFromOnNewIntent = isFromOnNewIntent,
|
||||
)
|
||||
}
|
||||
webLink?.uriValidate() == true -> {
|
||||
urlOpener.openUrl(webLink)
|
||||
}
|
||||
when {
|
||||
receivedDeepLink != null -> {
|
||||
deeplinkFactory.handleDeeplink(
|
||||
deeplinkUri = receivedDeepLink,
|
||||
coroutineScope = lifecycleScope,
|
||||
isFromOnNewIntent = isFromOnNewIntent,
|
||||
)
|
||||
}
|
||||
webLink?.uriValidate() == true -> {
|
||||
urlOpener.openUrl(webLink)
|
||||
}
|
||||
} else {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
|
|
@ -78,7 +77,6 @@ import com.tangem.wallet.BuildConfig
|
|||
import dagger.hilt.EntryPoints
|
||||
import kotlinx.coroutines.*
|
||||
import org.rekotlin.Store
|
||||
import kotlin.collections.set
|
||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
||||
|
||||
lateinit var store: Store<AppState>
|
||||
|
|
@ -220,9 +218,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
|||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
|
||||
private val onlineCardVerifier: OnlineCardVerifier
|
||||
get() = entryPoint.getOnlineCardVerifier()
|
||||
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory
|
||||
get() = entryPoint.getColdUserWalletBuilderFactory()
|
||||
|
||||
|
|
@ -367,7 +362,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
|||
clipboardManager = clipboardManager,
|
||||
settingsManager = settingsManager,
|
||||
uiMessageSender = uiMessageSender,
|
||||
onlineCardVerifier = onlineCardVerifier,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
|
||||
|
||||
/**
|
||||
|
|
@ -21,6 +22,15 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
|
|||
addParamsInterceptor(LinkedCardContextInterceptor(scanResponse))
|
||||
}
|
||||
|
||||
fun Analytics.setContext(userWallet: UserWallet) {
|
||||
setUserId(userWallet.walletId.stringValue)
|
||||
// TODO add product type for hot ([REDACTED_TASK_KEY])
|
||||
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erases the context
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import coil.executeBlocking
|
|||
import coil.request.ImageRequest
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.WEBLINK_KEY
|
||||
import com.tangem.core.deeplink.converter.PayloadToDeeplinkConverter
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
|
|
@ -38,11 +35,11 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
|
|||
|
||||
val notification = message.notification ?: return
|
||||
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
|
||||
val deeplink = PayloadToDeeplinkConverter.convert(message.data)
|
||||
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
putExtra(DEEPLINK_KEY, deeplink)
|
||||
putExtra(WEBLINK_KEY, message.data[WEBLINK_KEY])
|
||||
message.data.forEach {
|
||||
putExtra(it.key, it.value)
|
||||
}
|
||||
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -37,8 +37,7 @@ internal object LegacyMiddleware {
|
|||
)
|
||||
store.dispatchWithMain(
|
||||
DetailsAction.PrepareScreen(
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
scanResponse = selectedUserWallet.requireColdWallet().scanResponse,
|
||||
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
|
||||
initializedAppSettingsState = initializedAppSettingsStateContent,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,16 +16,13 @@ import com.tangem.crypto.bip39.Wordlist
|
|||
import com.tangem.data.card.sdk.CardSdkOwner
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager
|
||||
import com.tangem.datasource.utils.AddHeadersInterceptor
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
||||
import com.tangem.sdk.DefaultSessionViewDelegate
|
||||
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
|
||||
import com.tangem.sdk.extensions.*
|
||||
import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider
|
||||
import com.tangem.sdk.nfc.NfcManager
|
||||
|
|
@ -34,13 +31,6 @@ import com.tangem.tap.foregroundActivityObserver
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -55,11 +45,9 @@ import javax.inject.Singleton
|
|||
internal class DefaultCardSdkProvider @Inject constructor(
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val cardSdkFeatureToggles: CardSdkFeatureToggles,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
) : CardSdkProvider, CardSdkOwner {
|
||||
|
||||
private val observer = Observer()
|
||||
|
|
@ -70,27 +58,15 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
get() = holder?.sdk ?: tryToRegisterWithForegroundActivity()
|
||||
|
||||
init {
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.map {
|
||||
when (it[ApiConfig.ID.TangemTech.name]) {
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.MOCK,
|
||||
-> false
|
||||
ApiEnvironment.PROD,
|
||||
null,
|
||||
-> true
|
||||
}
|
||||
val mutableManager = apiConfigsManager as? MutableApiConfigsManager
|
||||
|
||||
mutableManager?.addListener(
|
||||
object : MutableApiConfigsManager.ApiConfigEnvChangeListener(id = ApiConfig.ID.TangemTech) {
|
||||
override fun onChange(environmentConfig: ApiEnvironmentConfig) {
|
||||
holder?.sdk?.config?.tangemApiBaseUrl = environmentConfig.baseUrl
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { isProd ->
|
||||
holder?.let {
|
||||
it.sdk.config.isTangemAttestationProdEnv = isProd
|
||||
}
|
||||
}
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.main))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
TangemApiServiceSettings.addInterceptors(
|
||||
AddHeadersInterceptor(
|
||||
|
|
@ -188,10 +164,8 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
keystoreManager = keystoreManager,
|
||||
wordlist = Wordlist.getWordlist(activity),
|
||||
config = config.apply {
|
||||
isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled
|
||||
|
||||
val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech)
|
||||
isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD
|
||||
tangemApiBaseUrl = apiConfig.baseUrl
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
|||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -49,7 +48,6 @@ internal object ActivityModule {
|
|||
appStateHolder: AppStateHolder,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): RampStateManager {
|
||||
|
|
@ -57,7 +55,6 @@ internal object ActivityModule {
|
|||
sellService = Provider { requireNotNull(appStateHolder.sellService) },
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
currenciesRepository = currenciesRepository,
|
||||
getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
|||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -72,7 +71,6 @@ internal object ManageTokensDomainModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
derivationsRepository: DerivationsRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
|
|
@ -84,7 +82,6 @@ internal object ManageTokensDomainModule {
|
|||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
derivationsRepository = derivationsRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import dagger.Module
|
||||
|
|
@ -62,21 +60,17 @@ object MarketsDomainModule {
|
|||
derivationsRepository: DerivationsRepository,
|
||||
marketsTokenRepository: MarketsTokenRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
): SaveMarketTokensUseCase {
|
||||
return SaveMarketTokensUseCase(
|
||||
derivationsRepository = derivationsRepository,
|
||||
marketsTokenRepository = marketsTokenRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,12 +94,10 @@ internal object StakingDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchStakingYieldBalanceUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
): FetchStakingYieldBalanceUseCase {
|
||||
return FetchStakingYieldBalanceUseCase(
|
||||
stakingRepository = stakingRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
)
|
||||
|
|
@ -224,4 +222,12 @@ internal object StakingDomainModule {
|
|||
): CheckAccountInitializedUseCase {
|
||||
return CheckAccountInitializedUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetActionRequirementAmountUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
): GetActionRequirementAmountUseCase {
|
||||
return GetActionRequirementAmountUseCase(stakingRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,7 @@ package com.tangem.tap.di.domain
|
|||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
|
||||
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
|
||||
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
|
||||
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
|
||||
import com.tangem.domain.swap.usecase.*
|
||||
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -73,4 +70,30 @@ internal object SwapDomainModule {
|
|||
swapErrorResolver = swapErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetSwapDataUseCase(
|
||||
swapRepositoryV2: SwapRepositoryV2,
|
||||
swapErrorResolver: SwapErrorResolver,
|
||||
): GetSwapDataUseCase {
|
||||
return GetSwapDataUseCase(
|
||||
swapRepositoryV2 = swapRepositoryV2,
|
||||
swapErrorResolver = swapErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapTransactionSentUseCase(
|
||||
swapRepositoryV2: SwapRepositoryV2,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
swapErrorResolver: SwapErrorResolver,
|
||||
): SwapTransactionSentUseCase {
|
||||
return SwapTransactionSentUseCase(
|
||||
swapRepositoryV2 = swapRepositoryV2,
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
swapErrorResolver = swapErrorResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,6 @@ internal object TokensDomainModule {
|
|||
@Singleton
|
||||
fun provideAddCryptoCurrenciesUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
|
|
@ -50,7 +49,6 @@ internal object TokensDomainModule {
|
|||
): AddCryptoCurrenciesUseCase {
|
||||
return AddCryptoCurrenciesUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
|
|
@ -63,19 +61,15 @@ internal object TokensDomainModule {
|
|||
@Singleton
|
||||
fun provideFetchTokenListUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
): FetchTokenListUseCase {
|
||||
return FetchTokenListUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +167,6 @@ internal object TokensDomainModule {
|
|||
@Singleton
|
||||
fun provideFetchCurrencyStatusUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
|
|
@ -182,7 +175,6 @@ internal object TokensDomainModule {
|
|||
): FetchCurrencyStatusUseCase {
|
||||
return FetchCurrencyStatusUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
|
|
@ -195,19 +187,15 @@ internal object TokensDomainModule {
|
|||
@Singleton
|
||||
fun provideFetchCardTokenListUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
): FetchCardTokenListUseCase {
|
||||
return FetchCardTokenListUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -256,20 +244,16 @@ internal object TokensDomainModule {
|
|||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
rampStateManager: RampStateManager,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
promoRepository: PromoRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
currencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
): GetCryptoCurrencyActionsUseCase {
|
||||
return GetCryptoCurrencyActionsUseCase(
|
||||
rampManager = rampStateManager,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
promoRepository = promoRepository,
|
||||
dispatchers = dispatchers,
|
||||
currencyStatusOperations = currencyStatusOperations,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.transaction.FeeRepository
|
|||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.tap.domain.hot.TangemHotSigner
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -43,6 +44,7 @@ internal object TransactionDomainModule {
|
|||
transactionRepository: TransactionRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
tangemHotSignerFactory: TangemHotSigner.Factory,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
demoConfig = DemoConfig(),
|
||||
|
|
@ -50,6 +52,7 @@ internal object TransactionDomainModule {
|
|||
transactionRepository = transactionRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
getHotSigner = tangemHotSignerFactory::create,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository
|
|||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.features.nft.NFTFeatureToggles
|
||||
import com.tangem.operations.attestation.CardArtworksProvider
|
||||
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -191,28 +188,14 @@ internal object WalletsDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCardImageUseCase(
|
||||
onlineCardVerifier: OnlineCardVerifier,
|
||||
cardArtworksProvider: CardArtworksProvider,
|
||||
cardSdkFeatureToggles: CardSdkFeatureToggles,
|
||||
): GetCardImageUseCase {
|
||||
return GetCardImageUseCase(
|
||||
verifier = onlineCardVerifier,
|
||||
cardArtworksProvider = cardArtworksProvider,
|
||||
cardSdkFeatureToggles = cardSdkFeatureToggles,
|
||||
)
|
||||
fun provideGetCardImageUseCase(cardArtworksProvider: CardArtworksProvider): GetCardImageUseCase {
|
||||
return GetCardImageUseCase(cardArtworksProvider = cardArtworksProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIsWalletNFTEnabledSyncUseCase(
|
||||
walletsRepository: WalletsRepository,
|
||||
nftFeatureToggles: NFTFeatureToggles,
|
||||
): IsWalletNFTEnabledSyncUseCase {
|
||||
return IsWalletNFTEnabledSyncUseCase(
|
||||
walletsRepository = walletsRepository,
|
||||
nftFeatureToggles = nftFeatureToggles,
|
||||
)
|
||||
fun providesIsWalletNFTEnabledSyncUseCase(walletsRepository: WalletsRepository): IsWalletNFTEnabledSyncUseCase {
|
||||
return IsWalletNFTEnabledSyncUseCase(walletsRepository = walletsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.tap.di.hot
|
||||
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
|
||||
import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester
|
||||
import com.tangem.tap.features.hot.TangemHotSDKProxy
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface TangemHotSdkModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.tap.di.routing
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.RoutingFeatureToggle
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.tap.routing.ProxyAppRouter
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.tap.routing.configurator.MutableAppRouterConfig
|
||||
|
|
@ -33,10 +31,4 @@ internal object AppRouterModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRoutingFeatureToggle(featureTogglesManager: FeatureTogglesManager): RoutingFeatureToggle {
|
||||
return RoutingFeatureToggle(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Wallet
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -35,14 +34,15 @@ class TapWalletManager(
|
|||
}
|
||||
|
||||
private suspend fun loadUserWalletData(userWallet: UserWallet) {
|
||||
Analytics.setContext(userWallet.requireColdWallet().scanResponse) // [REDACTED_TASK_KEY]
|
||||
val scanResponse = userWallet.scanResponse
|
||||
Analytics.setContext(userWallet)
|
||||
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
||||
|
||||
withMainContext {
|
||||
// Order is important
|
||||
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
||||
withMainContext {
|
||||
// Order is important
|
||||
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ internal class DefaultDerivationsRepository(
|
|||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -61,7 +61,11 @@ internal class DefaultDerivationsRepository(
|
|||
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
}
|
||||
|
||||
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return
|
||||
}
|
||||
|
||||
userWallet.requireColdWallet()
|
||||
|
||||
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
|
||||
Timber.d("Nothing to derive")
|
||||
|
|
@ -84,14 +88,18 @@ internal class DefaultDerivationsRepository(
|
|||
): Boolean {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return false
|
||||
}
|
||||
|
||||
val derivations =
|
||||
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
|
||||
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
package com.tangem.tap.domain.extensions
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.getTwinCardNumber
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.wallets.models.Artwork
|
||||
import com.tangem.operations.attestation.CardArtworksProvider
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
|
||||
|
||||
fun CardDTO.signedHashesCount(): Int {
|
||||
return wallets.sumOf { it.totalSignedHashes ?: 0 }
|
||||
}
|
||||
|
||||
suspend fun CardDTO.getOrLoadCardArtworkUrl(
|
||||
cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null,
|
||||
onlineCardVerifier: OnlineCardVerifier,
|
||||
): String {
|
||||
fun ifAnyError(): String {
|
||||
return when {
|
||||
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
|
||||
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
|
||||
else -> {
|
||||
when (getTwinCardNumber()) {
|
||||
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
|
||||
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
|
||||
else -> Artwork.DEFAULT_IMG_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when (val cardInfoResult = cardInfo ?: onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
|
||||
is Result.Success -> {
|
||||
val artworkId = cardInfoResult.data.artwork?.id
|
||||
if (artworkId.isNullOrEmpty()) {
|
||||
ifAnyError()
|
||||
} else {
|
||||
CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
|
||||
}
|
||||
}
|
||||
|
||||
is Result.Failure -> ifAnyError()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
) {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> {
|
||||
hotWalletPasswordRequester.requestPassword(hotWalletId)
|
||||
}
|
||||
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
tangemHotSdk.signHashes(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = auth,
|
||||
),
|
||||
dataToSign = dataToSign,
|
||||
)
|
||||
}.getOrElse {
|
||||
if (hotWalletId.authType == HotWalletId.AuthType.Biometry) {
|
||||
val passwordAuth = hotWalletPasswordRequester.requestPassword(hotWalletId)
|
||||
tangemHotSdk.signHashes(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = passwordAuth,
|
||||
),
|
||||
dataToSign = dataToSign,
|
||||
)
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
|
||||
interface HotWalletPasswordRequester {
|
||||
|
||||
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
class TangemHotSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TokensFeatureToggles {
|
||||
|
||||
override val isStakingLoadingRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED")
|
||||
|
||||
override val isWalletBalanceFetcherEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED")
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isLocked
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
|
|
@ -208,8 +207,7 @@ internal class BiometricUserWalletsListManager(
|
|||
changeSelectedUserWallet: Boolean,
|
||||
canOverridePublicInfo: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
val encryptionKey = userWallet.scanResponse.card.encryptionKey
|
||||
val encryptionKey = userWallet.encryptionKey
|
||||
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
|
||||
?: return CompletionResult.Success(Unit) // No encryption key, no need to save
|
||||
|
||||
|
|
|
|||
|
|
@ -2,31 +2,44 @@ package com.tangem.tap.domain.userWalletList.model
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.models.MobileWallet
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletSensitiveInformation(
|
||||
// Cold
|
||||
@Json(name = "wallets")
|
||||
val wallets: List<CardDTO.Wallet>,
|
||||
val wallets: List<CardDTO.Wallet>?,
|
||||
@Json(name = "visaCardActivationStatus")
|
||||
val visaCardActivationStatus: VisaCardActivationStatus? = null,
|
||||
// Hot
|
||||
@Json(name = "mobileWallets")
|
||||
val mobileWallets: List<MobileWallet>? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletPublicInformation(
|
||||
// Common
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "walletId")
|
||||
val walletId: UserWalletId,
|
||||
// Cold
|
||||
@Json(name = "cardsInWallet")
|
||||
val cardsInWallet: Set<String>,
|
||||
@Json(name = "scanResponse")
|
||||
val scanResponse: ScanResponse,
|
||||
val scanResponse: ScanResponse?,
|
||||
@Json(name = "isMultiCurrency")
|
||||
val isMultiCurrency: Boolean,
|
||||
@Json(name = "hasBackupError")
|
||||
val hasBackupError: Boolean = false,
|
||||
// Hot
|
||||
@Json(name = "hotWalletId")
|
||||
val hotWalletId: HotWalletId? = null,
|
||||
@Json(name = "backedUp")
|
||||
val backedUp: Boolean? = null,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.domain.userWalletList.utils
|
|||
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
|
|
@ -10,8 +11,12 @@ internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
|
|||
is UserWallet.Cold -> UserWalletSensitiveInformation(
|
||||
wallets = scanResponse.card.wallets,
|
||||
visaCardActivationStatus = scanResponse.visaCardActivationStatus,
|
||||
mobileWallets = null,
|
||||
)
|
||||
is UserWallet.Hot -> UserWalletSensitiveInformation(
|
||||
wallets = null,
|
||||
mobileWallets = this.wallets,
|
||||
)
|
||||
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
|
||||
}
|
||||
|
||||
internal val UserWallet.publicInformation: UserWalletPublicInformation
|
||||
|
|
@ -28,19 +33,39 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
|
|||
visaCardActivationStatus = null,
|
||||
),
|
||||
hasBackupError = hasBackupError,
|
||||
hotWalletId = null,
|
||||
backedUp = null,
|
||||
)
|
||||
is UserWallet.Hot -> UserWalletPublicInformation(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
cardsInWallet = emptySet(),
|
||||
scanResponse = null,
|
||||
hotWalletId = hotWalletId,
|
||||
backedUp = backedUp,
|
||||
)
|
||||
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
|
||||
}
|
||||
|
||||
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
|
||||
return UserWallet.Cold(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
hasBackupError = hasBackupError,
|
||||
)
|
||||
return if (hotWalletId != null) {
|
||||
UserWallet.Hot(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
hotWalletId = hotWalletId,
|
||||
wallets = null,
|
||||
backedUp = backedUp!!,
|
||||
)
|
||||
} else {
|
||||
UserWallet.Cold(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse!!,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
hasBackupError = hasBackupError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet> {
|
||||
|
|
@ -53,13 +78,15 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
|
|||
copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
card = scanResponse.card.copy(
|
||||
wallets = sensitiveInformation.wallets,
|
||||
wallets = sensitiveInformation.wallets!!,
|
||||
),
|
||||
visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
|
||||
is UserWallet.Hot -> copy(
|
||||
wallets = sensitiveInformation.mobileWallets,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
import com.tangem.domain.models.MobileWallet
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
internal val CardDTO.encryptionKey: ByteArray?
|
||||
get() = findPublicKey(wallets)?.let { calculateEncryptionKey(it) }
|
||||
internal val UserWallet.encryptionKey: ByteArray?
|
||||
get() = when (this) {
|
||||
is UserWallet.Cold -> findPublicKey(this.scanResponse.card.wallets)
|
||||
is UserWallet.Hot -> findPublicKey(this.wallets.orEmpty())
|
||||
}?.let { calculateEncryptionKey(it) }
|
||||
|
||||
private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
|
||||
val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray()
|
||||
|
|
@ -15,8 +20,12 @@ private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
|
|||
}
|
||||
|
||||
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
|
||||
return wallets.firstOrNull()
|
||||
?.publicKey
|
||||
return wallets.firstOrNull()?.publicKey
|
||||
}
|
||||
|
||||
@JvmName("findPublicKeyInMobileWallets")
|
||||
private fun findPublicKey(wallets: List<MobileWallet>): ByteArray? {
|
||||
return wallets.firstOrNull()?.publicKey
|
||||
}
|
||||
|
||||
private const val MESSAGE_FOR_ENCRYPTION_KEY = "UserWalletEncryptionKey"
|
||||
|
|
@ -14,7 +14,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -422,8 +421,7 @@ class WalletConnectInteractor(
|
|||
}
|
||||
|
||||
private fun getCardId(userWallet: UserWallet): String? {
|
||||
userWallet.requireColdWallet() // [REDACTED_TASK_KEY]
|
||||
return if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
|
||||
return if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.backupStatus?.isActive != true) {
|
||||
userWallet.cardId
|
||||
} else { // if wallet has backup, any card from wallet can be used to sign
|
||||
null
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import org.rekotlin.Action
|
|||
sealed class DetailsAction : Action {
|
||||
|
||||
data class PrepareScreen(
|
||||
val scanResponse: ScanResponse,
|
||||
val scanResponse: ScanResponse?,
|
||||
val initializedAppSettingsState: AppSettingsState,
|
||||
) : DetailsAction()
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ internal class DefaultCardSettingsComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
CardSettingsScreen(modifier = modifier, state = state)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import com.tangem.tap.common.analytics.events.Settings
|
|||
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.domain.extensions.signedHashesCount
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardInfo
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
|
||||
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
|
||||
|
|
@ -75,6 +74,8 @@ internal class CardSettingsModel @Inject constructor(
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Reset card scanned data
|
||||
cardSettingsInteractor.clear()
|
||||
// Restore the previous value of access code request policy
|
||||
cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy
|
||||
}
|
||||
|
|
@ -169,6 +170,8 @@ internal class CardSettingsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CardDTO.signedHashesCount(): Int = wallets.sumOf { it.totalSignedHashes ?: 0 }
|
||||
|
||||
private fun handleClickingItem(item: CardInfo) {
|
||||
when (item) {
|
||||
is CardInfo.ChangeAccessCode -> {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import arrow.core.getOrElse
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
|
|
@ -31,12 +32,18 @@ internal class WalletConnectModel @Inject constructor(
|
|||
|
||||
init {
|
||||
modelScope.launch {
|
||||
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
|
||||
listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT)
|
||||
.getOrElse { emptyFlow() }
|
||||
.map {
|
||||
.map { result ->
|
||||
val source = when (result.resultSource) {
|
||||
QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD
|
||||
QrResultSource.CAMERA,
|
||||
QrResultSource.GALLERY,
|
||||
-> WalletConnectAction.OpenSession.SourceType.QR
|
||||
}
|
||||
WalletConnectAction.OpenSession(
|
||||
wcUri = it,
|
||||
source = WalletConnectAction.OpenSession.SourceType.QR,
|
||||
wcUri = result.qrCode,
|
||||
source = source,
|
||||
userWalletId = params.userWalletId,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
|
|||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
|
||||
import com.tangem.core.ui.utils.findActivity
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.home.api.HomeComponent
|
||||
import com.tangem.tap.features.home.compose.StoriesScreen
|
||||
|
|
@ -29,6 +30,7 @@ import org.rekotlin.StoreSubscriber
|
|||
internal class DefaultHomeComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
|
||||
|
||||
private val model: HomeModel = getOrCreateModel()
|
||||
|
|
@ -58,7 +60,7 @@ internal class DefaultHomeComponent @AssistedInject constructor(
|
|||
val activity = LocalContext.current.findActivity()
|
||||
BackHandler(onBack = activity::finish)
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
if (homeState.value.isV2StoriesEnabled) {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
StoriesScreenV2(
|
||||
homeState = homeState,
|
||||
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import org.rekotlin.StateType
|
|||
// todo refactor [REDACTED_TASK_KEY]
|
||||
data class HomeState(
|
||||
val scanInProgress: Boolean = false,
|
||||
val isV2StoriesEnabled: Boolean = false,
|
||||
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
|
||||
) : StateType {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.features.hot
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester {
|
||||
|
||||
override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password {
|
||||
return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.tap.features.hot
|
||||
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Proxy for [TangemHotSdk] to allow lazy initialization and provide a way to access the SDK state from domain layer.
|
||||
* SDK is initialized in the [com.tangem.tap.routing.component.RoutingComponent] and can be accessed through this proxy.
|
||||
* Be aware that the SDK is initialized on activity creation, so it may not be available immediately.
|
||||
*/
|
||||
@Singleton
|
||||
class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
|
||||
|
||||
val sdkState = MutableStateFlow<TangemHotSdk?>(null)
|
||||
|
||||
override suspend fun importWallet(mnemonic: Mnemonic, passphrase: CharArray?, auth: HotAuth): HotWalletId =
|
||||
callSdk { importWallet(mnemonic, passphrase, auth) }
|
||||
|
||||
override suspend fun generateWallet(auth: HotAuth, mnemonicType: MnemonicType): HotWalletId =
|
||||
callSdk { generateWallet(auth, mnemonicType) }
|
||||
|
||||
override suspend fun exportMnemonic(unlockHotWallet: UnlockHotWallet): SeedPhrasePrivateInfo =
|
||||
callSdk { exportMnemonic(unlockHotWallet) }
|
||||
|
||||
override suspend fun exportBackup(unlockHotWallet: UnlockHotWallet): ByteArray =
|
||||
callSdk { exportBackup(unlockHotWallet) }
|
||||
|
||||
override suspend fun delete(id: HotWalletId) = callSdk { delete(id) }
|
||||
|
||||
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =
|
||||
callSdk { changeAuth(unlockHotWallet, auth) }
|
||||
|
||||
override suspend fun derivePublicKey(
|
||||
unlockHotWallet: UnlockHotWallet,
|
||||
request: DeriveWalletRequest,
|
||||
): DerivedPublicKeyResponse = callSdk { derivePublicKey(unlockHotWallet, request) }
|
||||
|
||||
override suspend fun signHashes(unlockHotWallet: UnlockHotWallet, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
callSdk { signHashes(unlockHotWallet, dataToSign) }
|
||||
|
||||
private suspend fun <T> callSdk(block: suspend TangemHotSdk.() -> T): T {
|
||||
return withTimeout(timeMillis = 1000) {
|
||||
sdkState.filterNotNull().first()
|
||||
}.block()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,11 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.common.keyboard.KeyboardValidator
|
||||
import com.tangem.common.routing.RoutingFeatureToggle
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.coil.ImagePreloader
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -46,7 +44,6 @@ import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCas
|
|||
import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase
|
||||
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
|
||||
import com.tangem.feature.swap.analytics.StoriesEvents
|
||||
import com.tangem.features.onramp.deeplink.OnrampDeepLink
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
|
||||
|
|
@ -85,8 +82,6 @@ internal class MainViewModel @Inject constructor(
|
|||
private val imagePreloader: ImagePreloader,
|
||||
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val deepLinksRegistry: DeepLinksRegistry,
|
||||
private val onrampDeepLinkFactory: OnrampDeepLink.Factory,
|
||||
private val notificationsToggles: NotificationsFeatureToggles,
|
||||
private val getApplicationIdUseCase: GetApplicationIdUseCase,
|
||||
private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase,
|
||||
|
|
@ -97,7 +92,6 @@ internal class MainViewModel @Inject constructor(
|
|||
private val multiQuoteUpdater: MultiQuoteUpdater,
|
||||
private val appStateHolder: AppStateHolder,
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
routingFeatureToggle: RoutingFeatureToggle,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -140,10 +134,6 @@ internal class MainViewModel @Inject constructor(
|
|||
sendKeyboardIdentifierEvent()
|
||||
|
||||
preloadImages()
|
||||
|
||||
if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
|
||||
initializeDeepLinks()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
@ -200,7 +190,7 @@ internal class MainViewModel @Inject constructor(
|
|||
userWalletsListManager.selectedUserWallet
|
||||
.distinctUntilChanged()
|
||||
.onEach { userWallet ->
|
||||
Analytics.setContext(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
|
||||
Analytics.setContext(userWallet)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
|
|
@ -417,7 +407,7 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex.message)
|
||||
Timber.e(ex)
|
||||
analyticsEventHandler.send(
|
||||
StoriesEvents.Error(
|
||||
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
|
||||
|
|
@ -426,10 +416,6 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun initializeDeepLinks() {
|
||||
deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope))
|
||||
}
|
||||
|
||||
private suspend fun initPushNotifications() {
|
||||
if (notificationsToggles.isNotificationsEnabled) {
|
||||
getApplicationIdUseCase().onRight { applicationId ->
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.domain.common.util.cardTypesResolver
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.legacy.unlockIfLockable
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.extensions.*
|
||||
|
|
@ -95,7 +95,7 @@ internal class WelcomeMiddleware {
|
|||
}
|
||||
.doOnSuccess { selectedUserWallet ->
|
||||
sendSignedInAnalyticsEvent(
|
||||
scanResponse = selectedUserWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = selectedUserWallet,
|
||||
signInType = Basic.SignedIn.SignInType.Biometric,
|
||||
)
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ internal class WelcomeMiddleware {
|
|||
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error))
|
||||
}
|
||||
.doOnSuccess {
|
||||
sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card)
|
||||
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedIn.SignInType.Card)
|
||||
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
||||
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
|
||||
|
|
@ -142,7 +142,14 @@ internal class WelcomeMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun sendSignedInAnalyticsEvent(scanResponse: ScanResponse, signInType: Basic.SignedIn.SignInType) {
|
||||
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
return
|
||||
}
|
||||
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val currency = ParamCardCurrencyConverter().convert(
|
||||
value = scanResponse.cardTypesResolver,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,27 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
|
||||
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
|
||||
|
||||
override fun getCardPublicKey(): String {
|
||||
return userWalletsListManager.selectedUserWalletSync
|
||||
?.requireColdWallet()?.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
|
||||
val userWallet = userWalletsListManager.selectedUserWalletSync
|
||||
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return userWallet.scanResponse.card.cardPublicKey.toHexString()
|
||||
}
|
||||
|
||||
override fun getCardId(): String {
|
||||
return userWalletsListManager.selectedUserWalletSync
|
||||
?.requireColdWallet()?.scanResponse?.card?.cardId ?: ""
|
||||
val userWallet = userWalletsListManager.selectedUserWalletSync
|
||||
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return userWallet.scanResponse.card.cardId
|
||||
}
|
||||
|
||||
override fun getCardsPublicKeys(): Map<String, String> {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -25,10 +24,8 @@ internal class CryptoCurrencyConverter(
|
|||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = value.blockchain,
|
||||
extraDerivationPath = value.derivationPath,
|
||||
scanResponse = requireNotNull(
|
||||
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync
|
||||
?.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
?.scanResponse,
|
||||
userWallet = requireNotNull(
|
||||
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -37,10 +34,8 @@ internal class CryptoCurrencyConverter(
|
|||
sdkToken = value.token,
|
||||
blockchain = value.blockchain,
|
||||
extraDerivationPath = value.derivationPath,
|
||||
scanResponse = requireNotNull(
|
||||
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync
|
||||
?.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
?.scanResponse,
|
||||
userWallet = requireNotNull(
|
||||
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import com.tangem.domain.core.lce.Lce
|
|||
import com.tangem.domain.exchange.ExpressAvailabilityState
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -32,7 +30,6 @@ internal class DefaultRampManager(
|
|||
private val sellService: Provider<ExchangeService>,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : RampStateManager {
|
||||
|
|
@ -40,11 +37,10 @@ internal class DefaultRampManager(
|
|||
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
|
||||
|
||||
override suspend fun availableForBuy(
|
||||
scanResponse: ScanResponse,
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): ScenarioUnavailabilityReason {
|
||||
val availabilityState = runCatching { getOnrampAvailableState(userWalletId, cryptoCurrency) }
|
||||
val availabilityState = runCatching { getOnrampAvailableState(userWallet.walletId, cryptoCurrency) }
|
||||
.getOrNull()
|
||||
?: ExpressAvailabilityState.Error
|
||||
|
||||
|
|
@ -52,8 +48,9 @@ internal class DefaultRampManager(
|
|||
}
|
||||
|
||||
override suspend fun availableForSell(
|
||||
userWallet: UserWallet,
|
||||
userWalletId: UserWalletId,
|
||||
status: CryptoCurrencyStatus,
|
||||
sendUnavailabilityReason: ScenarioUnavailabilityReason?,
|
||||
): Either<ScenarioUnavailabilityReason, Unit> {
|
||||
return either {
|
||||
val sellSupportedByService = catch(
|
||||
|
|
@ -65,7 +62,8 @@ internal class DefaultRampManager(
|
|||
catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) },
|
||||
)
|
||||
|
||||
val reason = getSendUnavailabilityReason(userWallet = userWallet, cryptoCurrencyStatus = status)
|
||||
val reason = sendUnavailabilityReason
|
||||
?: getSendUnavailabilityReason(userWalletId = userWalletId, cryptoCurrencyStatus = status)
|
||||
|
||||
ensure(condition = reason is ScenarioUnavailabilityReason.None) {
|
||||
when (reason) {
|
||||
|
|
@ -111,6 +109,27 @@ internal class DefaultRampManager(
|
|||
return expressServiceLoader.getInitializationStatus(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getSendUnavailabilityReason(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): ScenarioUnavailabilityReason {
|
||||
return when {
|
||||
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
|
||||
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
|
||||
}
|
||||
currenciesRepository.isSendBlockedByPendingTransactions(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
) -> {
|
||||
ScenarioUnavailabilityReason.PendingTransaction(
|
||||
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
|
||||
networkName = cryptoCurrencyStatus.currency.network.name,
|
||||
)
|
||||
}
|
||||
else -> ScenarioUnavailabilityReason.None
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExchangeableState(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -179,33 +198,4 @@ internal class DefaultRampManager(
|
|||
val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}
|
||||
|
||||
private suspend fun getSendUnavailabilityReason(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): ScenarioUnavailabilityReason {
|
||||
val coinStatus = getNetworkCoinStatusUseCase.invokeSync(
|
||||
userWallet = userWallet,
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
|
||||
).getOrNull()
|
||||
|
||||
return when {
|
||||
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
|
||||
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
|
||||
}
|
||||
currenciesRepository.isSendBlockedByPendingTransactions(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
coinStatus = coinStatus,
|
||||
) -> {
|
||||
ScenarioUnavailabilityReason.PendingTransaction(
|
||||
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
|
||||
networkName = coinStatus?.currency?.network?.name.orEmpty(),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ScenarioUnavailabilityReason.None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.attestation.CardArtworksProvider
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
|
|
@ -75,7 +74,6 @@ data class DaggerGraphState(
|
|||
val clipboardManager: ClipboardManager? = null,
|
||||
val settingsManager: SettingsManager? = null,
|
||||
val uiMessageSender: UiMessageSender? = null,
|
||||
val onlineCardVerifier: OnlineCardVerifier? = null,
|
||||
val cardArworksProvider: CardArtworksProvider? = null,
|
||||
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
|
||||
val userTokensResponseStore: UserTokensResponseStore? = null,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.ChildStack
|
|||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
|
|
@ -16,7 +17,10 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.features.walletconnect.components.WcRoutingComponent
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.android.create
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.features.hot.TangemHotSDKProxy
|
||||
import com.tangem.tap.routing.RootContent
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.component.RoutingComponent.Child
|
||||
|
|
@ -36,6 +40,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val uiDependencies: UiDependencies,
|
||||
private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
|
||||
private val deeplinkFactory: DeepLinkFactory,
|
||||
private val tangemHotSDKProxy: TangemHotSDKProxy,
|
||||
) : RoutingComponent,
|
||||
AppComponentContext by context,
|
||||
SnackbarHandler {
|
||||
|
|
@ -70,6 +75,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
appRouterConfig.stack = stackItems
|
||||
}
|
||||
}
|
||||
|
||||
configureHotSdk()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -120,6 +127,17 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
initialStack
|
||||
}
|
||||
|
||||
private fun configureHotSdk() {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity)
|
||||
},
|
||||
onDestroy = {
|
||||
tangemHotSDKProxy.sdkState.value = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : RoutingComponent.Factory {
|
||||
override fun create(context: AppComponentContext, initialStack: List<AppRoute>?): DefaultRoutingComponent
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.tangem.features.send.v2.api.NFTSendComponent
|
|||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.wallet.WalletEntryComponent
|
||||
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
||||
|
|
@ -86,7 +85,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
|
||||
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
|
||||
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
|
||||
private val testerRouter: TesterRouter,
|
||||
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
|
||||
) {
|
||||
|
||||
|
|
@ -132,9 +130,6 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = welcomeComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.TesterMenu -> {
|
||||
Child.LegacyIntent(testerRouter.getEntryIntent())
|
||||
}
|
||||
is AppRoute.WalletSettings -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -370,7 +365,7 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.PushNotification -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = Unit,
|
||||
params = PushNotificationsComponent.Params.Route(AppRoute.Home),
|
||||
componentFactory = pushNotificationsComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ internal class DefaultDerivationsRepositoryTest {
|
|||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf),
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
|
|
@ -129,7 +129,7 @@ internal class DefaultDerivationsRepositoryTest {
|
|||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf),
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { error("Should throws exception") }
|
||||
|
|
@ -155,7 +155,7 @@ internal class DefaultDerivationsRepositoryTest {
|
|||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf),
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.configs.GenericCardConfig
|
||||
import com.tangem.domain.common.configs.MultiWalletCardConfig
|
||||
|
|
@ -34,9 +35,10 @@ internal class MissedDerivationsFinderTest {
|
|||
fun `empty derivations for non supported blockchains`() {
|
||||
// Bls is not supported
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).chia.let(::listOf)
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
|
@ -55,9 +57,10 @@ internal class MissedDerivationsFinderTest {
|
|||
),
|
||||
)
|
||||
}
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).chiaAndEthereum
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
|
|
@ -69,9 +72,10 @@ internal class MissedDerivationsFinderTest {
|
|||
@Test
|
||||
fun `derivations for custom token`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumTokenWithBinanceDerivation
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
|
|
@ -86,9 +90,10 @@ internal class MissedDerivationsFinderTest {
|
|||
@Test
|
||||
fun `derivations for cardano`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).cardano.let(::listOf)
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
|
|
@ -111,9 +116,10 @@ internal class MissedDerivationsFinderTest {
|
|||
cardConfig = Wallet2CardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).ethereum.let(::listOf)
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
|
@ -125,9 +131,10 @@ internal class MissedDerivationsFinderTest {
|
|||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(scanResponse)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumAndStellar
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
|
|
|
|||
|
|
@ -30,4 +30,10 @@ dependencies {
|
|||
api(deps.kotlin.serialization)
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.timber)
|
||||
|
||||
/* Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
}
|
||||
|
|
@ -30,10 +30,6 @@ sealed class AppRoute(val path: String) : Route {
|
|||
) : AppRoute(path = "/welcome"), RouteBundleParams {
|
||||
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
|
||||
companion object {
|
||||
const val INITIAL_INTENT_KEY = "intent"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
@ -177,9 +173,6 @@ sealed class AppRoute(val path: String) : Route {
|
|||
"/$isInitialReverseOrder",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object TesterMenu : AppRoute(path = "/tester_menu")
|
||||
|
||||
@Serializable
|
||||
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.common.routing
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
class RoutingFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
|
||||
val isDeepLinkNavigationEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "DEEPLINK_NAVIGATION_ENABLED")
|
||||
}
|
||||
|
|
@ -1,51 +1,40 @@
|
|||
package com.tangem.core.deeplink.converter
|
||||
package com.tangem.common.routing.deeplink
|
||||
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TANGEM_SCHEME
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TANGEM_SCHEME
|
||||
|
||||
/**
|
||||
* Builder class for constructing deep links with a fluent interface.
|
||||
*/
|
||||
/** Builder class for constructing deep links with a fluent interface */
|
||||
internal class DeepLinkBuilder {
|
||||
|
||||
private var scheme: String = TANGEM_SCHEME
|
||||
private var action: String = ""
|
||||
private val pathParams: MutableList<String> = mutableListOf()
|
||||
private val queryParams: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Sets the scheme for the deep link (e.g., "tangem", "https")
|
||||
*/
|
||||
/** Sets the scheme for the deep link (e.g., "tangem", "https") */
|
||||
fun setScheme(scheme: String): DeepLinkBuilder {
|
||||
this.scheme = scheme
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action for the deep link (e.g., "link", "wallet")
|
||||
*/
|
||||
/** Sets the action for the deep link (e.g., "link", "wallet") */
|
||||
fun setAction(action: String): DeepLinkBuilder {
|
||||
this.action = action
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a path parameter to the deep link
|
||||
*/
|
||||
/** Adds a path parameter to the deep link */
|
||||
fun addPathParam(param: String): DeepLinkBuilder {
|
||||
pathParams.add(param)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a query parameter to the deep link
|
||||
*/
|
||||
/** Adds a query parameter to the deep link */
|
||||
fun addQueryParam(key: String, value: String): DeepLinkBuilder {
|
||||
queryParams[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the deep link URI string
|
||||
*/
|
||||
/** Builds the deep link URI string */
|
||||
fun build(): String {
|
||||
val path = if (pathParams.isEmpty()) {
|
||||
action
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.core.deeplink
|
||||
package com.tangem.common.routing.deeplink
|
||||
|
||||
object DeeplinkConst {
|
||||
const val DEEPLINK_KEY = "deeplink"
|
||||
const val WEBLINK_KEY = "link"
|
||||
|
||||
const val TANGEM_SCHEME = "tangem"
|
||||
const val WALLET_ID_KEY = "user_wallet_id"
|
||||
const val NETWORK_ID_KEY = "network_id"
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.core.deeplink.converter
|
||||
package com.tangem.common.routing.deeplink
|
||||
|
||||
import android.os.Bundle
|
||||
import com.tangem.common.routing.DeepLinkRoute
|
||||
import com.tangem.common.routing.DeepLinkScheme
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NAME_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NAME_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
||||
|
|
@ -22,13 +23,25 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
}
|
||||
}
|
||||
|
||||
fun convertBundle(bundle: Bundle?): String? {
|
||||
if (bundle == null) return null
|
||||
val bundleDataMap = mutableMapOf<String, String>()
|
||||
for (key in bundle.keySet()) {
|
||||
val value = bundle.getString(key)
|
||||
if (value != null) {
|
||||
bundleDataMap[key] = value
|
||||
}
|
||||
}
|
||||
return convert(bundleDataMap)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun buildNotificationDeeplink(payload: Map<String, String>): String? {
|
||||
val type = payload[TYPE_KEY] ?: return null
|
||||
val networkId = payload[NETWORK_ID_KEY] ?: return null
|
||||
val tokenId = payload[TOKEN_ID_KEY] ?: return null
|
||||
val walletId = payload[WALLET_ID_KEY] ?: return null
|
||||
val derivationPath = payload[DERIVATION_PATH_KEY] ?: return null
|
||||
val derivationPath = payload[DERIVATION_PATH_KEY].orEmpty()
|
||||
val transactionId = payload[TRANSACTION_ID_KEY]
|
||||
val name = payload[NAME_KEY]
|
||||
|
||||
|
|
@ -38,7 +51,9 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
addQueryParam(TOKEN_ID_KEY, tokenId)
|
||||
addQueryParam(TYPE_KEY, type)
|
||||
addQueryParam(WALLET_ID_KEY, walletId)
|
||||
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
|
||||
if (derivationPath.isNotBlank()) {
|
||||
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
|
||||
}
|
||||
|
||||
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
|
||||
name?.let { addQueryParam(NAME_KEY, it) }
|
||||
|
|
@ -49,7 +64,6 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
return payload.containsKey(TYPE_KEY) &&
|
||||
payload.containsKey(NETWORK_ID_KEY) &&
|
||||
payload.containsKey(TOKEN_ID_KEY) &&
|
||||
payload.containsKey(WALLET_ID_KEY) &&
|
||||
payload.containsKey(DERIVATION_PATH_KEY)
|
||||
payload.containsKey(WALLET_ID_KEY)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.core.deeplink.converter
|
||||
package com.tangem.common.routing.deeplink
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.deeplink.DeeplinkConst
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.core.deeplink.converter
|
||||
package com.tangem.common.routing.deeplink
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import org.junit.Test
|
||||
|
||||
internal class PayloadToDeeplinkConverterTest {
|
||||
|
|
@ -48,6 +48,25 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN push notification payload without derivationPath WHEN convert THEN should return correct deeplink without derivation_path`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to "token",
|
||||
NETWORK_ID_KEY to "ethereum",
|
||||
TOKEN_ID_KEY to "0x123",
|
||||
WALLET_ID_KEY to "wallet123",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN push notification payload with missing type WHEN convert THEN should return null`() {
|
||||
// GIVEN
|
||||
|
|
@ -6,18 +6,19 @@ import com.tangem.blockchain.common.Token
|
|||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.configs.GenericCardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = defaultScanResponse) {
|
||||
class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaultUserWallet) {
|
||||
|
||||
private val factory = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains())
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
|
|||
val derivationPath = createDerivationPath(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
cardDerivationStyleProvider = scanResponse.derivationStyleProvider,
|
||||
cardDerivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
|
||||
val network = Network(
|
||||
|
|
@ -72,7 +73,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
|
|||
fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency {
|
||||
val derivationPath = Network.DerivationPath.Custom(
|
||||
value = derivationBlockchain.derivationPath(
|
||||
scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)!!.rawPath,
|
||||
)
|
||||
|
||||
|
|
@ -114,7 +115,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
|
|||
),
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = scanResponse,
|
||||
userWallet = userWallet,
|
||||
)!!
|
||||
}
|
||||
|
||||
|
|
@ -161,9 +162,11 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
|
|||
|
||||
private companion object {
|
||||
|
||||
val defaultScanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(2),
|
||||
derivedKeys = emptyMap(),
|
||||
val defaultUserWallet = MockUserWalletFactory.create(
|
||||
scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(2),
|
||||
derivedKeys = emptyMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -165,12 +165,12 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
|
|||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
),
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isLocked
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
|
|
@ -57,13 +56,19 @@ class UserWalletItemUMConverter(
|
|||
}
|
||||
|
||||
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
|
||||
userWallet.requireColdWallet()
|
||||
val cardCount = userWallet.getCardsCount() ?: 1
|
||||
val text = TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = cardCount,
|
||||
formatArgs = wrappedList(cardCount),
|
||||
)
|
||||
val text = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val cardCount = userWallet.getCardsCount() ?: 1
|
||||
TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = cardCount,
|
||||
formatArgs = wrappedList(cardCount),
|
||||
)
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
TextReference.Res(R.string.hw_mobile_wallet)
|
||||
}
|
||||
}
|
||||
return UserWalletItemUM.Information.Loaded(text)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ sealed class AnalyticsParam {
|
|||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object HotWallet : ScreensSources("Hot Wallet")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -11,18 +11,6 @@
|
|||
"name": "STAKING_TON_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NFT_ENABLED",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "NFT_EVM_ENABLED",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "NFT_SOLANA_ENABLED",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "NFT_MEDIA_CONTENT_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -31,26 +19,10 @@
|
|||
"name": "STAKING_CARDANO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ARTWORK_LOADING",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ATTESTATION_ENABLED",
|
||||
"version": "5.24.0"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_LOADING_REFACTORING_ENABLED",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "DEEPLINK_NAVIGATION_ENABLED",
|
||||
"version": "5.25.0"
|
||||
},
|
||||
{
|
||||
"name": "PUSH_NOTIFICATIONS_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -74,5 +46,9 @@
|
|||
{
|
||||
"name": "WALLET_BALANCE_FETCHER_ENABLED",
|
||||
"version": "5.27.0"
|
||||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ enum class ApiEnvironment {
|
|||
@Json(name = "DEV")
|
||||
DEV,
|
||||
|
||||
@Json(name = "DEV_2")
|
||||
DEV_2,
|
||||
|
||||
@Json(name = "STAGE")
|
||||
STAGE,
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal class Express(
|
|||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createDevEnvironment(),
|
||||
createDev2Environment(),
|
||||
createStageEnvironment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
|
|
@ -53,6 +54,12 @@ internal class Express(
|
|||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createDev2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV_2,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class TangemTech(
|
|||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem.org/v1/",
|
||||
baseUrl = "https://api.tangem.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ internal class DevApiConfigsManager(
|
|||
private val apiConfigs: ApiConfigs,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MutableApiConfigsManager {
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
|
|
@ -34,22 +34,22 @@ internal class DevApiConfigsManager(
|
|||
override fun initialize() {
|
||||
isInitialized.value = false
|
||||
|
||||
// We can't use appPreferencesStore.getObjectMap as base flow,
|
||||
// because we should keep possibility to work with configs synchronous.
|
||||
// See [getBaseUrl]
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.distinctUntilChanged()
|
||||
.onEach { savedEnvironments ->
|
||||
configs.update { apiConfigs ->
|
||||
apiConfigs.mapValues {
|
||||
val (config, currentEnvironment) = it
|
||||
val apiConfigs = configs.value
|
||||
|
||||
savedEnvironments[config.id.name] ?: currentEnvironment
|
||||
}
|
||||
configs.value = apiConfigs.mapValues {
|
||||
val (config, currentEnvironment) = it
|
||||
|
||||
savedEnvironments[config.id.name] ?: currentEnvironment
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
||||
notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments)
|
||||
}
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
|
||||
}
|
||||
|
|
@ -96,6 +96,35 @@ internal class DevApiConfigsManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun notifyListeners(
|
||||
apiConfigs: Map<ApiConfig, ApiEnvironment>,
|
||||
savedEnvironments: Map<String, ApiEnvironment>,
|
||||
) {
|
||||
if (registerListeners.isNotEmpty()) {
|
||||
val changedConfigs = apiConfigs.mapNotNull { (config, prevEnvironment) ->
|
||||
val newEnvironment = savedEnvironments[config.id.name] ?: config.defaultEnvironment
|
||||
|
||||
if (prevEnvironment == newEnvironment) return@mapNotNull null
|
||||
|
||||
val environmentConfig = config.environmentConfigs
|
||||
.firstOrNull { it.environment == newEnvironment }
|
||||
?: return@mapNotNull null
|
||||
|
||||
config.id to environmentConfig
|
||||
}
|
||||
|
||||
if (changedConfigs.isNotEmpty()) {
|
||||
registerListeners.forEach { listener ->
|
||||
val changedConfig = changedConfigs.firstOrNull { it.first == listener.id }?.second
|
||||
|
||||
if (changedConfig != null) {
|
||||
listener.onChange(changedConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
|
||||
return apiConfigs.associateWith(ApiConfig::defaultEnvironment)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import com.tangem.datasource.api.common.config.ApiConfig
|
|||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in MOCK environment
|
||||
|
|
@ -17,13 +18,16 @@ import kotlinx.coroutines.flow.update
|
|||
*/
|
||||
internal class MockApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
) : MutableApiConfigsManager {
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : MutableApiConfigsManager() {
|
||||
|
||||
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
|
||||
field = MutableStateFlow(value = getInitialConfigs())
|
||||
|
||||
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
@ -55,6 +59,20 @@ internal class MockApiConfigsManager(
|
|||
}
|
||||
}
|
||||
|
||||
override fun addListener(listener: ApiConfigEnvChangeListener) {
|
||||
super.addListener(listener)
|
||||
|
||||
configs
|
||||
.map { it.entries.firstOrNull { it.key.id == listener.id } }
|
||||
.filterNotNull()
|
||||
.onEach { (apiConfig, currentEnvironment) ->
|
||||
listener.onChange(
|
||||
environmentConfig = apiConfig.environmentConfigs.first { it.environment == currentEnvironment },
|
||||
)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
|
||||
return apiConfigs.associateWith { it.defaultEnvironment }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
|
|||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
@ -9,14 +10,41 @@ import kotlinx.coroutines.flow.Flow
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MutableApiConfigsManager : ApiConfigsManager {
|
||||
abstract class MutableApiConfigsManager : ApiConfigsManager {
|
||||
|
||||
/** Api configs with current [ApiEnvironment] */
|
||||
val configs: Flow<Map<ApiConfig, ApiEnvironment>>
|
||||
abstract val configs: Flow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
||||
/**
|
||||
* A set of listeners registered to observe changes in API environment configurations.
|
||||
* These listeners are notified whenever an environment change occurs.
|
||||
*/
|
||||
protected val registerListeners: Set<ApiConfigEnvChangeListener>
|
||||
field = mutableSetOf<ApiConfigEnvChangeListener>()
|
||||
|
||||
/** Change api environment [environment] by [id] */
|
||||
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
|
||||
abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
|
||||
|
||||
/** Change api environment [environment] for all configs */
|
||||
suspend fun changeEnvironment(environment: ApiEnvironment)
|
||||
abstract suspend fun changeEnvironment(environment: ApiEnvironment)
|
||||
|
||||
/** Adds a [listener] to observe changes in API environment configurations */
|
||||
open fun addListener(listener: ApiConfigEnvChangeListener) {
|
||||
registerListeners += listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for observing changes in API environment configurations
|
||||
*
|
||||
* @property id the identifier of the API configuration this listener is associated with
|
||||
*/
|
||||
abstract class ApiConfigEnvChangeListener(val id: ApiConfig.ID) {
|
||||
|
||||
/**
|
||||
* Called when the environment configuration changes
|
||||
*
|
||||
* @param environmentConfig the updated [ApiEnvironmentConfig] for the associated API configuration
|
||||
*/
|
||||
abstract fun onChange(environmentConfig: ApiEnvironmentConfig)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import retrofit2.http.Query
|
|||
interface TangemTechMarketsApi {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@GET("coins/list")
|
||||
@GET("v1/coins/list")
|
||||
suspend fun getCoinsList(
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
|
|
@ -20,24 +20,24 @@ interface TangemTechMarketsApi {
|
|||
@Query("timestamp") timestamp: Long?,
|
||||
): ApiResponse<TokenMarketListResponse>
|
||||
|
||||
@GET("coins/{coin_id}")
|
||||
@GET("v1/coins/{coin_id}")
|
||||
suspend fun getCoinMarketData(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("language") language: String,
|
||||
): ApiResponse<TokenMarketInfoResponse>
|
||||
|
||||
@GET("coins/{coin_id}/history")
|
||||
@GET("v1/coins/{coin_id}/history")
|
||||
suspend fun getCoinChart(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartResponse>
|
||||
|
||||
@GET("coins/{coin_id}/exchanges")
|
||||
@GET("v1/coins/{coin_id}/exchanges")
|
||||
suspend fun getCoinExchanges(@Path("coin_id") coinId: String): ApiResponse<TokenMarketExchangesResponse>
|
||||
|
||||
@GET("coins/history_preview")
|
||||
@GET("v1/coins/history_preview")
|
||||
suspend fun getCoinsListCharts(
|
||||
@Query("coin_ids") coinIds: String,
|
||||
@Query("currency") currency: String,
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ data class OnrampQuoteResponse(
|
|||
val providerId: String,
|
||||
|
||||
@Json(name = "minFromAmount")
|
||||
val minFromAmount: String,
|
||||
val minFromAmount: String?,
|
||||
|
||||
@Json(name = "maxFromAmount")
|
||||
val maxFromAmount: String,
|
||||
val maxFromAmount: String?,
|
||||
|
||||
@Json(name = "minToAmount")
|
||||
val minToAmount: String?,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit
|
|||
@Suppress("TooManyFunctions")
|
||||
interface TangemTechApi {
|
||||
|
||||
@GET("coins")
|
||||
@GET("v1/coins")
|
||||
suspend fun getCoins(
|
||||
@Header("Cache-Control") cacheControl: String = "max-age=600",
|
||||
@Query("contractAddress") contractAddress: String? = null,
|
||||
|
|
@ -30,150 +30,150 @@ interface TangemTechApi {
|
|||
@Query("limit") limit: Int? = null,
|
||||
): ApiResponse<CoinsResponse>
|
||||
|
||||
@GET("rates")
|
||||
@GET("v1/rates")
|
||||
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
|
||||
|
||||
@GET("currencies")
|
||||
@GET("v1/currencies")
|
||||
suspend fun getCurrencyList(
|
||||
@Header("Cache-Control") cacheControl: String = "max-age=600",
|
||||
): ApiResponse<CurrenciesResponse>
|
||||
|
||||
@GET("geo")
|
||||
@GET("v1/geo")
|
||||
suspend fun getUserCountryCode(): GeoResponse
|
||||
|
||||
@GET("user-tokens/{user-id}")
|
||||
@GET("v1/user-tokens/{user-id}")
|
||||
suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse<UserTokensResponse>
|
||||
|
||||
@PUT("user-tokens/{user-id}")
|
||||
@PUT("v1/user-tokens/{user-id}")
|
||||
suspend fun saveUserTokens(
|
||||
@Path(value = "user-id") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("user-tokens")
|
||||
@POST("v1/user-tokens")
|
||||
suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse<Unit>
|
||||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("referral/{walletId}")
|
||||
@GET("v1/referral/{walletId}")
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||
|
||||
/** Make user referral, requires [StartReferralBody] */
|
||||
@POST("referral")
|
||||
@POST("v1/referral")
|
||||
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse<ReferralResponse>
|
||||
|
||||
@GET("quotes")
|
||||
@GET("v1/quotes")
|
||||
suspend fun getQuotes(
|
||||
@Query("currencyId") currencyId: String,
|
||||
@Query("coinIds") coinIds: String,
|
||||
@Query("fields") fields: String,
|
||||
): ApiResponse<QuotesResponse>
|
||||
|
||||
@GET("promotion")
|
||||
@GET("v1/promotion")
|
||||
suspend fun getPromotionInfo(
|
||||
@Query("programName") name: String,
|
||||
@Header("Cache-Control") cacheControl: String = "max-age=600",
|
||||
): ApiResponse<PromotionInfoResponse>
|
||||
|
||||
@GET("settings/{wallet_id}")
|
||||
@GET("v1/settings/{wallet_id}")
|
||||
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
|
||||
|
||||
@PUT("settings/{wallet_id}")
|
||||
@PUT("v1/settings/{wallet_id}")
|
||||
suspend fun saveUserTokensSettings(
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body userTokensSettings: UserTokensSettingsResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("user-network-account")
|
||||
@POST("v1/user-network-account")
|
||||
suspend fun createUserNetworkAccount(
|
||||
@Body body: CreateUserNetworkAccountBody,
|
||||
): ApiResponse<CreateUserNetworkAccountResponse>
|
||||
|
||||
@POST("account")
|
||||
@POST("v1/account")
|
||||
suspend fun createUserTokensAccount(
|
||||
@Body body: CreateUserTokensAccountBody,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}")
|
||||
@PUT("v1/account/{account_id}")
|
||||
suspend fun updateUserTokensAccount(
|
||||
@Path("account_id") accountId: Int,
|
||||
@Body body: UpdateUserTokensAccountBody,
|
||||
): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}/archive")
|
||||
@PUT("v1/account/{account_id}/archive")
|
||||
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@PUT("account/{account_id}/unarchive")
|
||||
@PUT("v1/account/{account_id}/unarchive")
|
||||
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
|
||||
|
||||
@GET("features")
|
||||
@GET("v1/features")
|
||||
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
|
||||
|
||||
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
|
||||
@GET("networks/providers")
|
||||
@GET("v1/networks/providers")
|
||||
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
|
||||
|
||||
@GET("seedphrase-notification/{wallet_id}")
|
||||
@GET("v1/seedphrase-notification/{wallet_id}")
|
||||
suspend fun getSeedPhraseNotificationStatus(
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<SeedPhraseNotificationDTO>
|
||||
|
||||
@PUT("seedphrase-notification/{wallet_id}")
|
||||
@PUT("v1/seedphrase-notification/{wallet_id}")
|
||||
suspend fun updateSeedPhraseNotificationStatus(
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body body: SeedPhraseNotificationDTO,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("seedphrase-notification/{wallet_id}/confirmed")
|
||||
@GET("v1/seedphrase-notification/{wallet_id}/confirmed")
|
||||
suspend fun getSeedPhraseSecondNotificationStatus(
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<SeedPhraseNotificationDTO>
|
||||
|
||||
@PUT("seedphrase-notification/{wallet_id}/confirmed")
|
||||
@PUT("v1/seedphrase-notification/{wallet_id}/confirmed")
|
||||
suspend fun updateSeedPhraseSecondNotificationStatus(
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body body: SeedPhraseNotificationDTO,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("hot_crypto")
|
||||
@GET("v1/hot_crypto")
|
||||
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
|
||||
|
||||
@GET("stories/{story_id}")
|
||||
@GET("v1/stories/{story_id}")
|
||||
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
|
||||
|
||||
// region push notifications
|
||||
@GET("notification/push_notifications_eligible_networks")
|
||||
@GET("v1/notification/push_notifications_eligible_networks")
|
||||
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
|
||||
|
||||
@POST("user-wallets/applications/")
|
||||
@POST("v1/user-wallets/applications/")
|
||||
suspend fun createApplicationId(
|
||||
@Body
|
||||
body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<NotificationApplicationIdResponse>
|
||||
|
||||
@PATCH("user-wallets/applications/{application_id}")
|
||||
@PATCH("v1/user-wallets/applications/{application_id}")
|
||||
suspend fun updatePushTokenForApplicationId(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PATCH("user-wallets/wallets/{wallet_id}/notify")
|
||||
@PATCH("v1/user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// region wallets
|
||||
@PATCH("user-wallets/wallets/{wallet_id}")
|
||||
@PATCH("v1/user-wallets/wallets/{wallet_id}")
|
||||
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
|
||||
@POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
|
||||
@POST("v1/user-wallets/wallets/create-and-connect-by-appuid/{application_id}")
|
||||
suspend fun associateApplicationIdWithWallets(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: List<WalletIdBody>,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("user-wallets/wallets/{wallet_id}")
|
||||
@GET("v1/user-wallets/wallets/{wallet_id}")
|
||||
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
|
||||
|
||||
@GET("user-wallets/wallets/by-app/{app_id}")
|
||||
@GET("v1/user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ internal object NetworkModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ApiConfigsManager {
|
||||
return when {
|
||||
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs)
|
||||
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, dispatchers)
|
||||
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
|
||||
else -> ProdApiConfigsManager(apiConfigs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import com.squareup.moshi.Moshi
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.token.*
|
||||
import com.tangem.datasource.local.token.DefaultStakingActionsStore
|
||||
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
|
||||
import com.tangem.datasource.local.token.StakingActionsStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
|
|
@ -66,17 +68,6 @@ internal object StakingStoreModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingBalanceStore(
|
||||
persistenceStore: DataStore<Map<String, Set<YieldBalanceWrapperDTO>>>,
|
||||
): StakingBalanceStore {
|
||||
return DefaultStakingBalanceStore(
|
||||
persistenceStore = persistenceStore,
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingActionsStore(): StakingActionsStore {
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore.StakingID
|
||||
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal typealias YieldBalanceWrappersDTO = Map<String, Set<YieldBalanceWrapperDTO>>
|
||||
internal typealias YieldBalanceListByWalletId = Map<UserWalletId, Set<YieldBalance>>
|
||||
|
||||
/**
|
||||
* Default implementation of [StakingBalanceStore]
|
||||
*
|
||||
* @property persistenceStore persistence store
|
||||
* @property runtimeStore runtime store
|
||||
*/
|
||||
internal class DefaultStakingBalanceStore(
|
||||
private val persistenceStore: DataStore<YieldBalanceWrappersDTO>,
|
||||
private val runtimeStore: RuntimeSharedStore<YieldBalanceListByWalletId>,
|
||||
) : StakingBalanceStore {
|
||||
|
||||
override fun get(userWalletId: UserWalletId, stakingIds: List<StakingID>): Flow<Set<YieldBalance>> = channelFlow {
|
||||
val cachedBalances = persistenceStore.data
|
||||
.map {
|
||||
val wrappers = it[userWalletId.stringValue].orEmpty()
|
||||
.filter { wrapper ->
|
||||
stakingIds.any { id ->
|
||||
id.address == wrapper.addresses.address && id.integrationId == wrapper.integrationId
|
||||
}
|
||||
}
|
||||
|
||||
YieldBalanceConverter(isCached = true).convertSet(input = wrappers)
|
||||
}
|
||||
.firstOrNull()
|
||||
.orEmpty()
|
||||
|
||||
if (cachedBalances.isNotEmpty()) {
|
||||
send(cachedBalances)
|
||||
}
|
||||
|
||||
runtimeStore.get()
|
||||
.map {
|
||||
it[userWalletId].orEmpty().filter { balance ->
|
||||
stakingIds.any { id ->
|
||||
id.address == balance.address && id.integrationId == balance.integrationId
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
.onEach {
|
||||
val mergedBalances = mergeYieldBalances(
|
||||
stakingIds = stakingIds,
|
||||
cachedBalances = cachedBalances,
|
||||
runtimeBalances = it,
|
||||
)
|
||||
|
||||
send(mergedBalances)
|
||||
}
|
||||
.launchIn(scope = this)
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow<YieldBalance?> {
|
||||
return get(userWalletId = userWalletId, stakingIds = listOf(stakingID)).map { balances ->
|
||||
balances.getBalance(stakingID = stakingID)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>? {
|
||||
val runtimeBalances = runtimeStore.getSyncOrNull()?.getValue(userWalletId).orEmpty()
|
||||
val cachedBalances = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue).orEmpty()
|
||||
|
||||
if (runtimeBalances.isEmpty() && cachedBalances.isEmpty()) return null
|
||||
|
||||
return cachedBalances.mapTo(hashSetOf()) {
|
||||
val cached = YieldBalanceConverter(source = StatusSource.ONLY_CACHE).convert(value = it)
|
||||
val runtime = runtimeBalances.getBalance(address = cached.address, integrationId = cached.integrationId)
|
||||
|
||||
if (runtime == null || runtime is YieldBalance.Error) cached else runtime
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List<StakingID>): Set<YieldBalance>? {
|
||||
val runtime = runtimeStore.getSyncOrNull()?.getValue(userWalletId)
|
||||
val cached = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue)
|
||||
|
||||
if (runtime.isNullOrEmpty() && cached.isNullOrEmpty()) return null
|
||||
|
||||
return mergeYieldBalances(
|
||||
cachedBalances = YieldBalanceConverter(source = StatusSource.ONLY_CACHE)
|
||||
.convertSet(input = cached.orEmpty()),
|
||||
runtimeBalances = runtime.orEmpty(),
|
||||
stakingIds = stakingIds,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? {
|
||||
val balances = getSyncOrNull(userWalletId = userWalletId, stakingIds = listOf(stakingID)) ?: return null
|
||||
|
||||
return balances.getBalance(stakingID = stakingID)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
|
||||
coroutineScope {
|
||||
launch {
|
||||
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = items)
|
||||
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = saved[userWalletId]
|
||||
?.addOrReplace(newBalances) { old, new ->
|
||||
old.integrationId == new.integrationId && old.address == new.address
|
||||
}
|
||||
?: newBalances
|
||||
}
|
||||
}
|
||||
}
|
||||
launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refresh(userWalletId: UserWalletId, stakingIds: List<StakingID>) {
|
||||
updateRuntimeStore(userWalletId = userWalletId) { saved ->
|
||||
saved.mapTo(hashSetOf()) {
|
||||
val yieldBalance = it.takeIf { balance ->
|
||||
stakingIds.any { id -> balance.integrationId == id.integrationId && balance.address == id.address }
|
||||
}
|
||||
|
||||
yieldBalance?.copySealed(source = StatusSource.CACHE) ?: it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) {
|
||||
coroutineScope {
|
||||
launch {
|
||||
storeInRuntimeStore(
|
||||
userWalletId = userWalletId,
|
||||
integrationId = stakingID.integrationId,
|
||||
address = stakingID.address,
|
||||
item = item,
|
||||
)
|
||||
|
||||
storeInPersistenceStore(
|
||||
userWalletId = userWalletId,
|
||||
integrationId = stakingID.integrationId,
|
||||
address = stakingID.address,
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) {
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = saved[userWalletId]
|
||||
?.addOrReplace(item) { it.integrationId == item.integrationId && it.address == item.address }
|
||||
?: setOf(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntimeStore(
|
||||
userWalletId: UserWalletId,
|
||||
integrationId: String,
|
||||
address: String,
|
||||
item: YieldBalanceWrapperDTO,
|
||||
) {
|
||||
val newBalance = YieldBalanceConverter(isCached = false).convert(value = item)
|
||||
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = saved[userWalletId]
|
||||
?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address }
|
||||
?: setOf(newBalance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateRuntimeStore(
|
||||
userWalletId: UserWalletId,
|
||||
function: (Set<YieldBalance>) -> Set<YieldBalance>,
|
||||
) {
|
||||
runtimeStore.update(default = emptyMap()) { saved ->
|
||||
saved.toMutableMap().apply {
|
||||
this[userWalletId] = function(this[userWalletId].orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
|
||||
persistenceStore.updateData { current ->
|
||||
current.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = current[userWalletId.stringValue]
|
||||
?.addOrReplace(items = items) { old, new ->
|
||||
old.integrationId == new.integrationId && old.addresses.address == new.addresses.address
|
||||
}
|
||||
?: items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInPersistenceStore(
|
||||
userWalletId: UserWalletId,
|
||||
integrationId: String,
|
||||
address: String,
|
||||
item: YieldBalanceWrapperDTO,
|
||||
) {
|
||||
persistenceStore.updateData { current ->
|
||||
current.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = current[userWalletId.stringValue]
|
||||
?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address }
|
||||
?: setOf(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeYieldBalances(
|
||||
cachedBalances: Set<YieldBalance>,
|
||||
runtimeBalances: Set<YieldBalance>,
|
||||
stakingIds: List<StakingID>,
|
||||
): Set<YieldBalance> {
|
||||
return stakingIds.mapTo(hashSetOf()) { id ->
|
||||
val runtime = runtimeBalances.getBalance(stakingID = id)
|
||||
|
||||
if (runtime == null || runtime is YieldBalance.Error) {
|
||||
getCachedBalanceIfPossible(cachedBalances = cachedBalances, stakingID = id)
|
||||
} else {
|
||||
runtime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCachedBalanceIfPossible(cachedBalances: Set<YieldBalance>, stakingID: StakingID): YieldBalance {
|
||||
val cached = cachedBalances.getBalance(stakingID)
|
||||
?: return YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId)
|
||||
|
||||
val updatedCached = when (cached) {
|
||||
is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE)
|
||||
is YieldBalance.Empty -> cached.copy(source = StatusSource.ONLY_CACHE)
|
||||
is YieldBalance.Error,
|
||||
is YieldBalance.Unsupported,
|
||||
-> null
|
||||
}
|
||||
|
||||
return updatedCached ?: YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId)
|
||||
}
|
||||
|
||||
private fun Set<YieldBalance>.getBalance(stakingID: StakingID): YieldBalance? {
|
||||
return getBalance(address = stakingID.address, integrationId = stakingID.integrationId)
|
||||
}
|
||||
|
||||
private fun Set<YieldBalance>.getBalance(address: String?, integrationId: String?): YieldBalance? {
|
||||
return firstOrNull { yieldBalance ->
|
||||
val isCorrectAddress = address != null && address == yieldBalance.address
|
||||
val isCorrectIntegration = integrationId != null && yieldBalance.integrationId == integrationId
|
||||
|
||||
isCorrectIntegration && isCorrectAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/** Staking balance store */
|
||||
interface StakingBalanceStore {
|
||||
|
||||
/** Get flow of [YieldBalanceList] by [userWalletId] and [stakingIds] */
|
||||
fun get(userWalletId: UserWalletId, stakingIds: List<StakingID>): Flow<Set<YieldBalance>>
|
||||
|
||||
/** Get flow of [YieldBalance] by [userWalletId] and [stakingID] */
|
||||
fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow<YieldBalance?>
|
||||
|
||||
/** Get all [YieldBalance] synchronously or null by [userWalletId] */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>?
|
||||
|
||||
/** Get [YieldBalanceList] synchronously or null by [userWalletId] and [stakingIds] */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List<StakingID>): Set<YieldBalance>?
|
||||
|
||||
/** Get [YieldBalance] synchronously or null by [userWalletId] and [stakingID] */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance?
|
||||
|
||||
/** Store [items] by [userWalletId] */
|
||||
suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>)
|
||||
|
||||
/** Store [item] by [userWalletId] and [stakingID] */
|
||||
suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO)
|
||||
|
||||
/** Store [item] by [userWalletId] */
|
||||
suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance)
|
||||
|
||||
/** Refresh balances of [stakingIds] by [userWalletId] */
|
||||
suspend fun refresh(userWalletId: UserWalletId, stakingIds: List<StakingID>)
|
||||
|
||||
data class StakingID(val integrationId: String, val address: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ApiConfigTest {
|
||||
|
||||
@Test
|
||||
fun `all baseUrls ends with slash`() {
|
||||
// Arrange
|
||||
val allBaseUrls = createApiConfigs().flatMap { it.environmentConfigs.map { it.baseUrl } }
|
||||
|
||||
// Actual
|
||||
val actual = allBaseUrls.all { it.endsWith("/") }
|
||||
|
||||
Timber.e(allBaseUrls.joinToString(separator = "\n"))
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
private fun createApiConfigs(): ApiConfigs {
|
||||
return ApiConfig.ID.entries.mapTo(destination = hashSetOf()) {
|
||||
when (it) {
|
||||
ApiConfig.ID.Express -> {
|
||||
Express(
|
||||
environmentConfigStorage = mockk(),
|
||||
expressAuthProvider = mockk(),
|
||||
appVersionProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.TangemTech -> {
|
||||
TangemTech(
|
||||
appVersionProvider = mockk(),
|
||||
authProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
)
|
||||
}
|
||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,7 +162,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
id = ApiConfig.ID.TangemTech,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem.org/v1/",
|
||||
baseUrl = "https://api.tangem.org/",
|
||||
headers = mapOf(
|
||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class ActivityInstanceHolder<T : Any> {
|
||||
private var instance: T? = null
|
||||
|
||||
lateinit var instanceAccess: WeakReference<T>
|
||||
private set
|
||||
|
||||
fun set(instance: T) {
|
||||
this.instance = instance
|
||||
instanceAccess = WeakReference(instance)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
instance = null
|
||||
instanceAccess.clear()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> AppComponentContext.getOrCreateActivityInstanceHolder(
|
||||
noinline factory: (AppCompatActivity) -> T,
|
||||
): ActivityInstanceHolder<T> {
|
||||
val holder = instanceKeeper.getOrCreateSimple {
|
||||
ActivityInstanceHolder<T>()
|
||||
}
|
||||
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
holder.set(factory(activity))
|
||||
},
|
||||
onDestroy = {
|
||||
holder.clear()
|
||||
},
|
||||
)
|
||||
|
||||
return holder
|
||||
}
|
||||
1
core/deep-links/.gitignore
vendored
1
core/deep-links/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Common */
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/* Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/* Libs - AndroidX */
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/* Libs - Other */
|
||||
implementation(deps.timber)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/* Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
}
|
||||
1
core/deep-links/global/.gitignore
vendored
1
core/deep-links/global/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink.global"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project */
|
||||
implementation(projects.core.deepLinks)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
@Deprecated("Use ReferralDeepLinkHandler")
|
||||
class ReferralDeepLink(
|
||||
val onReceive: () -> Unit,
|
||||
) : DeepLink(shouldHandleDelayed = true) {
|
||||
override val uri: String = "tangem://referral"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
onReceive()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
@Deprecated("Use SellDeepLinkHandler")
|
||||
class SellCurrencyDeepLink(
|
||||
val onReceive: (data: Data) -> Unit,
|
||||
shouldHandleDelayed: Boolean,
|
||||
) : DeepLink(shouldHandleDelayed) {
|
||||
|
||||
override val uri: String = "tangem://redirect_sell"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
val data = Data(
|
||||
transactionId = params["transactionId"] ?: return,
|
||||
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
||||
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
||||
currencyId = params["currency_id"] ?: return,
|
||||
depositWalletAddressTag = params["depositWalletAddressTag"],
|
||||
)
|
||||
|
||||
onReceive(data)
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val transactionId: String,
|
||||
val baseCurrencyAmount: String,
|
||||
val depositWalletAddress: String,
|
||||
val currencyId: String,
|
||||
val depositWalletAddressTag: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
/**
|
||||
* Represents a deep link.
|
||||
*/
|
||||
abstract class DeepLink(val shouldHandleDelayed: Boolean = false) {
|
||||
|
||||
/**
|
||||
* ID of the deep link.
|
||||
*
|
||||
* By default, it is the same as the [uri].
|
||||
* */
|
||||
val id: String get() = uri
|
||||
|
||||
/**
|
||||
* URI of the deep link.
|
||||
*
|
||||
* **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.**
|
||||
*
|
||||
* Query parameters will be received automatically.
|
||||
*
|
||||
* Path parameters can be added using the following syntax:
|
||||
* ```kotlin
|
||||
* "tangem://link" // Without parameters
|
||||
* "tangem://link/{param1}/{param2}" // With path parameters
|
||||
* ```
|
||||
* */
|
||||
abstract val uri: String
|
||||
|
||||
/**
|
||||
* Method to be called when this deep link is received.
|
||||
*
|
||||
* @param params Map of parameters received from the deep link.
|
||||
* */
|
||||
abstract fun onReceive(params: Map<String, String>)
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Key to pass deeplink via intent
|
||||
*/
|
||||
const val DEEPLINK_KEY = "deeplink"
|
||||
const val WEBLINK_KEY = "link"
|
||||
|
||||
// TODO: Add tests
|
||||
/**
|
||||
* Provides functionality to handle deep links.
|
||||
*
|
||||
* Allows deep links to be launched, registered, or unregistered.
|
||||
*/
|
||||
interface DeepLinksRegistry {
|
||||
|
||||
/**
|
||||
* Finds matches registered deep links for the given [intent] and launches them.
|
||||
*
|
||||
* @return `true` if any deep link was received, `false` otherwise.
|
||||
*/
|
||||
fun launch(intent: Intent): Boolean
|
||||
|
||||
/**
|
||||
* Registers the given [deepLink].
|
||||
*/
|
||||
fun register(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Registers the given [deepLinks].
|
||||
*/
|
||||
fun register(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLinks].
|
||||
*/
|
||||
fun unregister(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLink].
|
||||
*/
|
||||
fun unregister(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Unregisters deep links with the given [ids].
|
||||
* */
|
||||
fun unregisterByIds(ids: Collection<String>)
|
||||
|
||||
/**
|
||||
* Triggers run last launched [Intent] with deeplink handlers that can handle delayed deeplink
|
||||
* of specific [deepLinkClass] after handle [Intent] clear that and second time no intent will be handled
|
||||
*/
|
||||
fun triggerDelayedDeeplink(deepLinkClass: Class<out DeepLink>)
|
||||
|
||||
fun cancelDelayedDeeplink()
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.core.deeplink.di
|
||||
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry
|
||||
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 DeepLinksModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeepLinksRegistry(): DeepLinksRegistry {
|
||||
return DefaultDeepLinksRegistry()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
package com.tangem.core.deeplink.impl
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
|
||||
|
||||
private var registries: List<DeepLink> = emptyList()
|
||||
private var lastDeepLink: Uri? = null
|
||||
|
||||
override fun launch(intent: Intent): Boolean {
|
||||
// Try to get deeplink from data (direct deeplink flow)
|
||||
// Otherwise, try to get from extras (notification deeplink flow)
|
||||
val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri()
|
||||
val received = intent.data ?: deepLinkExtras ?: return false
|
||||
lastDeepLink = received
|
||||
var hasMatch = false
|
||||
|
||||
Timber.i(
|
||||
"""
|
||||
Received deep link intent
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
registries.forEach { deepLink ->
|
||||
val expected = deepLink.uri.toUri()
|
||||
|
||||
if (!isMatches(expected, received)) return@forEach
|
||||
hasMatch = true
|
||||
|
||||
val params = getParams(expected, received)
|
||||
|
||||
logMatch(hasMatch, expected, received, params)
|
||||
|
||||
deepLink.onReceive(params)
|
||||
lastDeepLink = null // clear deeplink if it was handled
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
logMatch(hasMatch, null, received, null)
|
||||
}
|
||||
|
||||
return hasMatch
|
||||
}
|
||||
|
||||
override fun register(deepLinks: Collection<DeepLink>) {
|
||||
registries = (registries + deepLinks).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun register(deepLink: DeepLink) {
|
||||
registries = (registries + deepLink).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLinks: Collection<DeepLink>) {
|
||||
registries = registries.filter { it !in deepLinks }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLink: DeepLink) {
|
||||
registries = registries.filter { it.id != deepLink.id }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregisterByIds(ids: Collection<String>) {
|
||||
registries = registries.filter { it.id !in ids }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun triggerDelayedDeeplink(deepLinkClass: Class<out DeepLink>) {
|
||||
val received = lastDeepLink
|
||||
if (received != null) {
|
||||
var hasMatch = false
|
||||
registries
|
||||
.filterIsInstance(deepLinkClass)
|
||||
.forEach { deepLink ->
|
||||
if (!deepLink.shouldHandleDelayed) return@forEach
|
||||
val expected = deepLink.uri.toUri()
|
||||
if (!isMatches(expected, received)) return@forEach
|
||||
hasMatch = true
|
||||
|
||||
val params = getParams(expected, received)
|
||||
logMatch(hasMatch, expected, received, params)
|
||||
deepLink.onReceive(params)
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
logMatch(hasMatch, null, received, null)
|
||||
}
|
||||
lastDeepLink = null // clear deeplink in any case handle or not
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelDelayedDeeplink() {
|
||||
lastDeepLink = null
|
||||
}
|
||||
|
||||
private fun logMatch(hasMatch: Boolean, expected: Uri?, received: Uri?, params: Map<String, String>?) {
|
||||
if (hasMatch) {
|
||||
Timber.i(
|
||||
"""
|
||||
Matched deep link
|
||||
|- Expected URI: $expected
|
||||
|- Received URI: $received
|
||||
|- Params: $params
|
||||
""".trimIndent(),
|
||||
)
|
||||
} else {
|
||||
Timber.i(
|
||||
"""
|
||||
No match found for deep link
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMatches(received: Uri, expected: Uri): Boolean {
|
||||
if (received == expected) return true
|
||||
if (received.authority != expected.authority ||
|
||||
received.pathSegments.size != expected.pathSegments.size
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
!(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}"))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getParams(received: Uri, expected: Uri): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
receivedSegment.startsWith(prefix = "{") &&
|
||||
receivedSegment.endsWith(suffix = "}")
|
||||
) {
|
||||
val path = receivedSegment
|
||||
.replace(oldValue = "{", newValue = "")
|
||||
.replace(oldValue = "}", newValue = "")
|
||||
|
||||
params[path] = expectedSegment
|
||||
}
|
||||
}
|
||||
|
||||
expected.queryParameterNames.forEach { paramName ->
|
||||
expected.getQueryParameter(paramName)?.let { param ->
|
||||
params[paramName] = param
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.core.deeplink.utils
|
||||
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
|
||||
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, vararg deepLinks: DeepLink) {
|
||||
registerDeepLinks(registry, deepLinks.toList())
|
||||
}
|
||||
|
||||
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, deepLinks: Collection<DeepLink>) {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
registry.register(deepLinks)
|
||||
},
|
||||
onDestroy = {
|
||||
registry.unregister(deepLinks)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1316,7 +1316,7 @@
|
|||
<string name="warning_express_approval_in_progress_message">Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein.</string>
|
||||
<string name="warning_express_approval_in_progress_title">Genehmigung in Arbeit</string>
|
||||
<string name="warning_express_dust_message">Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">Du hast keine %s austauschbaren Coins in Deiner Liste</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">Sie haben keine Token in Ihrem Portfolio, gegen die %s getauscht werden kann. Bitte fügen Sie einen anderen Token hinzu, um den Tausch durchzuführen.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Keine Token zum Tauschen verfügbar</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Die Gebühr %s kann nicht gedeckt werden</string>
|
||||
|
|
|
|||
|
|
@ -1236,7 +1236,7 @@
|
|||
<string name="warning_express_approval_in_progress_message">La aprobación del intercambio está en progreso y se completará en breve</string>
|
||||
<string name="warning_express_approval_in_progress_title">Aprobación en proceso</string>
|
||||
<string name="warning_express_dust_message">El monto mínimo de transacción es %1$s. Asegúrese de que el saldo restante después del canje no sea inferior a %2$s.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">No tiene %s monedas negociables en su lista</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">No tienes tokens en tu portafolio a los que puedas intercambiar %s. Por favor, añade otro token para realizar el intercambio.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">No hay tokens disponibles para intercambiar</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">Para realizar una transacción necesita depositar %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">No se pueden cubrir %s tarifa</string>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@
|
|||
<string name="app_settings_theme_mode_system">Par défaut du système</string>
|
||||
<string name="app_settings_theme_selector_title">Thème</string>
|
||||
<string name="app_settings_title">Paramètres de l\'application</string>
|
||||
<string name="backup_complete_seed_description">Ces mots ne peuvent pas être récupérés en cas de perte. Assurez-vous de les conserver en lieu sûr.</string>
|
||||
<string name="backup_seed_caution">Ne partagez jamais ces mots. Quiconque les apprend peut voler toutes vos cryptomonnaies. Tangem ne vous les demandera jamais. Les %s mots ci-dessous constituent la phrase de récupération de votre portefeuille. Cette phrase vous permet de récupérer votre portefeuille en cas de perte de votre appareil.</string>
|
||||
<string name="balance_hidden_description">Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres</string>
|
||||
<string name="balance_hidden_do_not_show_button">Ne plus afficher</string>
|
||||
<string name="balance_hidden_got_it_button">Compris</string>
|
||||
|
|
@ -216,6 +218,7 @@
|
|||
<string name="common_unreachable">Inaccessible</string>
|
||||
<string name="common_unstake">Unstakez</string>
|
||||
<string name="common_utxo_validate_withdrawal_message_warning">En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant.</string>
|
||||
<string name="common_value_copied">Valeur copiée</string>
|
||||
<string name="common_week">semaine</string>
|
||||
<string name="common_with">avec</string>
|
||||
<string name="common_yes">Oui</string>
|
||||
|
|
@ -699,6 +702,7 @@
|
|||
<string name="organize_tokens_sort_by_balance">Par solde</string>
|
||||
<string name="organize_tokens_title">Organiser les jetons</string>
|
||||
<string name="organize_tokens_ungroup">Dégrouper</string>
|
||||
<string name="push_transactions_notifications_description">Recevez des alertes pour les transactions entrantes sur les réseaux pris en charge.</string>
|
||||
<string name="qr_scanner_camera_denied_gallery_button">Sélectionnez dans la galerie</string>
|
||||
<string name="qr_scanner_camera_denied_settings_button">Paramètres</string>
|
||||
<string name="qr_scanner_camera_denied_text">Vous n\'avez pas donné accès à votre caméra</string>
|
||||
|
|
@ -855,6 +859,7 @@
|
|||
<string name="send_validation_invalid_amount">Montant invalide</string>
|
||||
<string name="send_validation_invalid_fee">Les frais de commissions dépassent le solde</string>
|
||||
<string name="send_validation_invalid_total">Le total dépasse le solde</string>
|
||||
<string name="send_with_swap_confirm_title">Échanger et envoyer</string>
|
||||
<string name="sent_transaction_sent_title">Transaction envoyée</string>
|
||||
<string name="settings_card_settings_footer">Scannez la carte/ bague que vous souhaitez configurer.</string>
|
||||
<string name="settings_forget_wallet">Oublier le portefeuille</string>
|
||||
|
|
@ -1085,6 +1090,7 @@
|
|||
<string name="unlock_wallet_description_full">Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille</string>
|
||||
<string name="unsupported_wc_version">Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion.</string>
|
||||
<string name="user_push_notification_agreement_argument_one">Restez à jour avec les dernières fonctionnalités et actualités</string>
|
||||
<string name="user_push_notification_agreement_argument_three">Recevez des notifications des transactions entrantes</string>
|
||||
<string name="user_push_notification_agreement_argument_two">Soyez le premier informé des nouvelles promotions</string>
|
||||
<string name="user_push_notification_agreement_header">Souhaitez-vous utiliser les\nnotifications push?</string>
|
||||
<string name="user_wallet_list_add_button">Ajouter un nouveau portefeuille</string>
|
||||
|
|
@ -1157,6 +1163,7 @@
|
|||
<string name="wallet_promo_banner_button_title">Obtenez-le maintenant avec 10 % de réduction</string>
|
||||
<string name="wallet_promo_banner_description">Accédez à plus de 13 000 cryptomonnaies. Achetez, vendez, échangez et stakez en un seul clic.\nAssociez jusqu\'à trois cartes pour une sauvegarde.</string>
|
||||
<string name="wallet_promo_banner_title">Découvrez le Portefeuille Tangem</string>
|
||||
<string name="wallet_settings_push_notifications_description">Recevez des notifications sur les transactions entrantes du portefeuille et les mises à jour de Tangem.</string>
|
||||
<string name="wallet_settings_title">Paramètres du portefeuille</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
<string name="warning_access_denied_message">Utilisez %s ou scannez une carte/bague pour déverrouiller l\'accès à votre portefeuille</string>
|
||||
|
|
@ -1184,7 +1191,7 @@
|
|||
<string name="warning_express_approval_in_progress_message">L\'approbation de l\'échange est en cours et sera achevée sous peu</string>
|
||||
<string name="warning_express_approval_in_progress_title">Approbation en cours</string>
|
||||
<string name="warning_express_dust_message">Le montant minimum d\'échange est de %1$s. Veuillez vous assurer que le solde restant après l\'échange ne sera pas inférieur à %2$s.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">Vous n\'avez pas de pièces échangeables %s dans votre liste</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">Vous n’avez aucun jeton dans votre portefeuille pouvant recevoir %s en échange. Veuillez ajouter un autre jeton pour effectuer l’échange.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Aucun jeton disponible à échanger</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">Pour effectuer une transaction, vous devez déposer %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Impossible de couvrir %s frais</string>
|
||||
|
|
@ -1243,9 +1250,13 @@
|
|||
<string name="warning_some_networks_unreachable_message">Certains réseaux sont actuellement inaccessibles. Veuillez réessayer plus tard.</string>
|
||||
<string name="warning_some_networks_unreachable_title">Certains réseaux sont inaccessibles</string>
|
||||
<string name="warning_some_token_balances_not_updated">Certains soldes de jetons n\'ont pas pu être mis à jour</string>
|
||||
<string name="warning_stellar_token_trustline_not_enough_xlm">Pas assez de %s. Rechargez votre compte XLM pour associer ce jeton</string>
|
||||
<string name="warning_testnet_card_message">Il s\'agit d\'une carte Testnet. Elle ne peut pas traiter les transactions et ne doit être utilisée qu\'à des fins de test et de développement.</string>
|
||||
<string name="warning_testnet_card_title">À des fins de test uniquement</string>
|
||||
<string name="warning_token_balance_not_updated">Le solde peut être obsolète. Rafraîchissez la page.</string>
|
||||
<string name="wc_alert_audit_malicious_domain">Domaine malveillant</string>
|
||||
<string name="wc_alert_unsupported_dapps_description">Le portefeuille Tangem ne prend actuellement pas en charge %ss</string>
|
||||
<string name="wc_alert_unsupported_dapps_title">dApp non prise en charge</string>
|
||||
<string name="wc_connections">Connexions</string>
|
||||
<string name="wc_disconnect_all">Déconnecter tout</string>
|
||||
<string name="wc_disconnect_all_alert_desc">Texte sur la déconnexion de toutes les dApps</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
<string name="access_code_alert_skip_ok">とにかくスキップ</string>
|
||||
<string name="access_code_alert_skip_title">アクセスコードが設定されていません</string>
|
||||
<string name="access_code_check_title">アクセスコードを入力</string>
|
||||
<string name="access_code_check_warining_delete">アクセスコードが間違っています。あと%s回間違えるとホットウォレットが削除されます。</string>
|
||||
<string name="access_code_check_warining_lock">アクセスコードが間違っています。あと%s回入力エラーが発生するとアプリがロックされます。</string>
|
||||
<string name="access_code_check_warining_wait">アクセスコードが間違っています。 \n %s秒待ってから再試行してください。</string>
|
||||
<string name="access_code_confirm_description">続行するには、以前に入力したコードを確認してください</string>
|
||||
<string name="access_code_confirm_title">アクセスコードを再入力</string>
|
||||
<string name="access_code_create_description">ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。</string>
|
||||
|
|
@ -655,7 +658,7 @@
|
|||
<string name="onboarding_button_continue_wallet">ウォレットへ進む</string>
|
||||
<string name="onboarding_button_finalize_backup">バックアップを完了する</string>
|
||||
<string name="onboarding_button_receive_crypto">暗号資産を受け取る</string>
|
||||
<string name="onboarding_button_scan_origin_card">プライマリカードをスキャン</string>
|
||||
<string name="onboarding_button_scan_origin_card">メインカードまたはリングをスキャン</string>
|
||||
<string name="onboarding_button_skip_backup">スキップする</string>
|
||||
<string name="onboarding_button_what_does_it_mean">どのように機能しますか?</string>
|
||||
<string name="onboarding_create_wallet_body">すべての秘密鍵をカードまたはリング内で生成し、安全なウォレットを作成しましょう</string>
|
||||
|
|
@ -706,7 +709,7 @@
|
|||
<string name="onboarding_title_no_backup_cards">バックアップデバイスなし</string>
|
||||
<string name="onboarding_title_notifications">通知</string>
|
||||
<string name="onboarding_title_one_backup_card">バックアップデバイスが1つ追加されました</string>
|
||||
<string name="onboarding_title_scan_origin_card">カードを準備してください</string>
|
||||
<string name="onboarding_title_scan_origin_card">カードまたはリングを用意してください</string>
|
||||
<string name="onboarding_title_two_backup_cards">バックアップデバイス2つが追加されました</string>
|
||||
<string name="onboarding_top_up_body">始めるには、ウォレットに任意の金額を入金するだけです</string>
|
||||
<string name="onboarding_top_up_body_no_account_error">始めるには、ウォレットに%1$s %2$s以上入金するだけです</string>
|
||||
|
|
@ -924,6 +927,7 @@
|
|||
<string name="send_validation_invalid_amount">無効な金額</string>
|
||||
<string name="send_validation_invalid_fee">手数料が残高を超えています</string>
|
||||
<string name="send_validation_invalid_total">合計金額が残高を超えています</string>
|
||||
<string name="send_with_swap_confirm_title">スワップして送信</string>
|
||||
<string name="send_with_swap_notification_text">トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。</string>
|
||||
<string name="send_with_swap_recipient_amount_text">受信者に送信されます</string>
|
||||
<string name="send_with_swap_recipient_amount_title">受取金額</string>
|
||||
|
|
@ -1296,8 +1300,8 @@
|
|||
<string name="warning_express_approval_in_progress_message">スワップ承認は現在進行中で、まもなく完了する予定です。</string>
|
||||
<string name="warning_express_approval_in_progress_title">承認が進行中</string>
|
||||
<string name="warning_express_dust_message">最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">あなたのリストには、交換可能な %s トークンがありません。</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">スワップ可能なトークンがありません</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">ポートフォリオ内に%sとスワップ可能なトークンがありません。交換を有効にするには、別のトークンを追加してください。</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">互換性のあるトークンが追加されていません</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">取引を行うには、 %1$s %2$sを入金する必要があります。</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">%s 手数料を支払えません</string>
|
||||
<string name="warning_express_notification_invalid_reserve_amount_title">受け取る金額は、 %s 以上である必要があります。</string>
|
||||
|
|
@ -1375,6 +1379,8 @@
|
|||
<string name="wc_alert_unknown_error_description">エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
|
||||
<string name="wc_alert_unknown_error_description_no_error_code">問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
|
||||
<string name="wc_alert_unknown_error_title">不明なエラーが発生しました</string>
|
||||
<string name="wc_alert_unsupported_dapps_description">Tangemウォレットは、現在%sをサポートしていません。</string>
|
||||
<string name="wc_alert_unsupported_dapps_title">サポートされていないdApp</string>
|
||||
<string name="wc_alert_unsupported_method_description">エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
|
||||
<string name="wc_alert_unsupported_method_title">不明なエラーが発生しました</string>
|
||||
<string name="wc_alert_unsupported_networks_description">Tangemは現在%sで必要なネットワークをサポートしていません。</string>
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@
|
|||
<string name="onboarding_button_continue_wallet">Перейти к моему кошельку</string>
|
||||
<string name="onboarding_button_finalize_backup">Завершение бэкапа</string>
|
||||
<string name="onboarding_button_receive_crypto">Получить криптовалюту</string>
|
||||
<string name="onboarding_button_scan_origin_card">Сканировать основную карту</string>
|
||||
<string name="onboarding_button_scan_origin_card">Сканировать основную карту или кольцо</string>
|
||||
<string name="onboarding_button_skip_backup">Пропустить</string>
|
||||
<string name="onboarding_button_what_does_it_mean">Как это работает?</string>
|
||||
<string name="onboarding_create_wallet_body">Давайте сгенерируем все ключи на вашей карте или кольце и создадим безопасный кошелек</string>
|
||||
|
|
@ -676,7 +676,7 @@
|
|||
<string name="onboarding_title_no_backup_cards">Нет резервных устройств</string>
|
||||
<string name="onboarding_title_notifications">Уведомления</string>
|
||||
<string name="onboarding_title_one_backup_card">Добавлено одно резервное устройство</string>
|
||||
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту</string>
|
||||
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту или кольцо</string>
|
||||
<string name="onboarding_title_two_backup_cards">Добавлены два резервных девайса</string>
|
||||
<string name="onboarding_top_up_body">Пополните кошелек на любую сумму, чтобы начать пользоваться картой</string>
|
||||
<string name="onboarding_top_up_body_no_account_error">Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой</string>
|
||||
|
|
@ -723,7 +723,12 @@
|
|||
<string name="organize_tokens_title">Упорядочить токены</string>
|
||||
<string name="organize_tokens_ungroup">Список</string>
|
||||
<string name="push_notifications_more_info">Подробнее</string>
|
||||
<string name="push_notifications_permission_alert_description">Вы можете включить нотификации в настройках</string>
|
||||
<string name="push_notifications_permission_alert_negative_button">Включить позже</string>
|
||||
<string name="push_notifications_permission_alert_positive_button">Настройки</string>
|
||||
<string name="push_notifications_permission_alert_title">Подключить нотификации</string>
|
||||
<string name="push_transactions_notifications_description">Получайте уведомления о входящих транзакциях в поддерживаемых сетях.</string>
|
||||
<string name="push_transactions_notifications_title">Уведомления о транзакциях</string>
|
||||
<string name="qr_scanner_camera_denied_gallery_button">Выбрать из галереи</string>
|
||||
<string name="qr_scanner_camera_denied_settings_button">Настройки</string>
|
||||
<string name="qr_scanner_camera_denied_text">Вы не предоставили доступ к вашей камере</string>
|
||||
|
|
@ -1119,6 +1124,7 @@
|
|||
<string name="unlock_wallet_description_full">Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку</string>
|
||||
<string name="unsupported_wc_version">Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения.</string>
|
||||
<string name="user_push_notification_agreement_argument_one">Будьте в курсе новых функций и новостей</string>
|
||||
<string name="user_push_notification_agreement_argument_three">Получайте уведомления о входящих транзакциях</string>
|
||||
<string name="user_push_notification_agreement_argument_two">Узнавайте первым о новых акциях</string>
|
||||
<string name="user_push_notification_agreement_header">Хотите использовать Push-уведомления?</string>
|
||||
<string name="user_wallet_list_add_button">Добавить новый кошелек</string>
|
||||
|
|
@ -1174,6 +1180,7 @@
|
|||
<string name="wallet_promo_banner_button_title">Получить с 10% скидкой</string>
|
||||
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
|
||||
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
|
||||
<string name="wallet_settings_push_notifications_description">Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem.</string>
|
||||
<string name="wallet_settings_push_notifications_title">Уведомления о транзакциях</string>
|
||||
<string name="wallet_settings_title">Настройки кошелька</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
|
|
@ -1202,7 +1209,7 @@
|
|||
<string name="warning_express_approval_in_progress_message">Разрешение обмена в процессе и будет скоро завершено</string>
|
||||
<string name="warning_express_approval_in_progress_title">Разрешение в процессе</string>
|
||||
<string name="warning_express_dust_message">Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">У вас нет токенов в портфеле, на которые можно обменять %s. Пожалуйста, добавьте другой токен, чтобы выполнить обмен.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Нет доступных для обмена токенов</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Невозможно покрыть комиссию %s</string>
|
||||
|
|
@ -1268,6 +1275,7 @@
|
|||
<string name="warning_token_trustline_subtitle">Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив.</string>
|
||||
<string name="warning_token_trustline_title">Откройте Trustline</string>
|
||||
<string name="wc_alert_session_disconnected_description">Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect.</string>
|
||||
<string name="wc_alert_unsupported_dapps_title">Неподдерживаемый dApp</string>
|
||||
<string name="wc_alert_wrong_card_description">Выбрана не верная карта или кольцо</string>
|
||||
<string name="wc_common_address">Адрес</string>
|
||||
<string name="wc_common_connect">Подключение</string>
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@
|
|||
<string name="express_exchange_status_receiving">Очікуємо на депозит</string>
|
||||
<string name="express_exchange_status_receiving_active">Очікуємо на депозит...</string>
|
||||
<string name="express_exchange_status_refunded">Повернено</string>
|
||||
<string name="express_exchange_status_refunding">Повернення коштів</string>
|
||||
<string name="express_exchange_status_sending">Надсилаємо вам</string>
|
||||
<string name="express_exchange_status_sending_active">Надсилаємо вам...</string>
|
||||
<string name="express_exchange_status_sent">Надіслано</string>
|
||||
|
|
@ -370,6 +371,7 @@
|
|||
<string name="give_permission_swap_subtitle" formatted="false">Щоб продовжити, вам потрібно дозволити смарт-контракту %1s використовувати ваш %2s</string>
|
||||
<string name="give_permission_title">Надати дозвіл</string>
|
||||
<string name="give_permission_unlimited">Необмежено</string>
|
||||
<string name="home_button_create_new_wallet">Створити новий гаманець</string>
|
||||
<string name="home_button_order">Купити</string>
|
||||
<string name="home_button_scan">Сканувати</string>
|
||||
<string name="hot_crypto_add_token_subtitle">в %s</string>
|
||||
|
|
@ -572,10 +574,11 @@
|
|||
<string name="nft_receive_unsupported_types_description">cNFT і pNFT наразі не підтримуються. Будь ласка, не надсилайте їх на свій гаманець.</string>
|
||||
<string name="nft_send">Надіслати NFT</string>
|
||||
<string name="nft_traits_title">Риси</string>
|
||||
<string name="nft_untitled_collection">Безіменна колекція</string>
|
||||
<string name="nft_wallet_count">NFTs в %2$d колекціях%1$d</string>
|
||||
<plurals name="nft_wallet_count_android">
|
||||
<item quantity="one">%1$d NFT в %2$d колекціi</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="few">%1$d NFTs в %2$d колекціях</item>
|
||||
<item quantity="many"></item>
|
||||
<item quantity="other">%1$d NFTs в %2$d колекціях</item>
|
||||
</plurals>
|
||||
|
|
@ -720,6 +723,7 @@
|
|||
<string name="organize_tokens_ungroup">Список</string>
|
||||
<string name="push_notifications_more_info">Більше</string>
|
||||
<string name="push_notifications_permission_alert_title">Увімкнути сповіщення</string>
|
||||
<string name="push_transactions_notifications_description">Отримуйте сповіщення про вхідні транзакції в підтримуваних мережах.</string>
|
||||
<string name="qr_scanner_camera_denied_gallery_button">Виберіть з галереї</string>
|
||||
<string name="qr_scanner_camera_denied_settings_button">Налаштування</string>
|
||||
<string name="qr_scanner_camera_denied_text">Ви не надали доступ до своєї камери</string>
|
||||
|
|
@ -829,6 +833,9 @@
|
|||
<string name="send_max_fee">Комісія може сягати до</string>
|
||||
<string name="send_memo_destination_tag_error">Недопустимий Memo</string>
|
||||
<string name="send_network_fee_warning_title">Покриття мережевої комісії</string>
|
||||
<string name="send_nonce">Nonce</string>
|
||||
<string name="send_nonce_footer">Унікальний номер транзакції. Змініть його, щоб пришвидшити відправку або скасувати завислу операцію.</string>
|
||||
<string name="send_nonce_hint">Введіть nonce…</string>
|
||||
<string name="send_notification_exceed_balance_text">Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс</string>
|
||||
<string name="send_notification_exceed_balance_title">Сума перевищує баланс</string>
|
||||
<string name="send_notification_existential_deposit_text">Для збереження вашого акаунту у блокчейні та захисту від можливих ризиків необхідний баланс не менше %s. Ця сума залишиться на вашому рахунку та не може бути знята.</string>
|
||||
|
|
@ -837,6 +844,7 @@
|
|||
<string name="send_notification_fee_too_high_title">Встановлена комісія завелика</string>
|
||||
<string name="send_notification_high_fee_text">Через особливості мережі %1$s комісія за переказ всього балансу вища. Щоб зменшити комісію, Ви можете залишити %2$s.</string>
|
||||
<string name="send_notification_high_fee_title">Підвищена комісія</string>
|
||||
<string name="send_notification_invalid_amount_rent_destination">Рахунок одержувача не активований. Мінімальна сума переказу повинна бути не менша балансу, необхідного для покриття арендної плати: %1$s.</string>
|
||||
<string name="send_notification_invalid_amount_rent_fee">Баланс вашого рахунку не може бути меншим за орендну плату. Будь ласка, залиште на рахунку не менше %1$s або виведіть всі кошти.</string>
|
||||
<string name="send_notification_invalid_amount_text">Включена комісія перевищує суму переказу, що призводить до від’ємного значення</string>
|
||||
<string name="send_notification_invalid_amount_title">Недопустима сума</string>
|
||||
|
|
@ -1095,6 +1103,8 @@
|
|||
<string name="transaction_history_transaction_validator">валідатор: %s</string>
|
||||
<string name="transfer_min_amount_error">Мінімум %s</string>
|
||||
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Мінімальна сума транзакції становить %1$s.</string>
|
||||
<string name="tron_will_be_send_token_fee_description">Комісії мережі Tron для популярних токенів можуть бути вищими. Стейкінг TRX може допомогти знизити транзакційні витрати.</string>
|
||||
<string name="tron_will_be_send_token_fee_title">Заощаджуйте на Tron комісіях</string>
|
||||
<string name="try_to_load_data_again_button_title">Спробуйте знову</string>
|
||||
<string name="twin_error_same_card">Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d</string>
|
||||
<string name="twin_error_wrong_twin">Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу</string>
|
||||
|
|
@ -1111,6 +1121,7 @@
|
|||
<string name="unlock_wallet_description_full">Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця</string>
|
||||
<string name="unsupported_wc_version">Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення.</string>
|
||||
<string name="user_push_notification_agreement_argument_one">Будьте в курсі останніх функцій та новин</string>
|
||||
<string name="user_push_notification_agreement_argument_three">Отримуйте сповіщення про вхідні транзакції</string>
|
||||
<string name="user_push_notification_agreement_argument_two">Дізнавайтеся першими про нові акції</string>
|
||||
<string name="user_push_notification_agreement_header">Бажаєте використовувати Push-повідомлення?</string>
|
||||
<string name="user_wallet_list_add_button">Додати новий гаманець</string>
|
||||
|
|
@ -1122,6 +1133,7 @@
|
|||
<string name="user_wallet_list_rename_popup_title">Перейменування гаманця</string>
|
||||
<string name="user_wallet_list_unlock_all">Розблокувати все</string>
|
||||
<string name="user_wallet_list_unlock_all_with">Розблокувати все з %s</string>
|
||||
<string name="visa_onboarding_pin_not_accepted">PIN-код не прийнятий. Спробуйте ще раз або введіть інший код.</string>
|
||||
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступний. Спробуйте пізніше</string>
|
||||
<string name="wallet_balance_missing_derivation">Відскануйте картку або кільце</string>
|
||||
<string name="wallet_been_activated_message">Цей гаманець вже був активований раніше.\nЯкщо це було зроблено не вами, зверніться до служби підтримки.\nTangem ніколи не продає гаманці разом з попередньо згенерованим кодом доступу.</string>
|
||||
|
|
@ -1165,6 +1177,7 @@
|
|||
<string name="wallet_promo_banner_button_title">Отримати з 10% знижкою</string>
|
||||
<string name="wallet_promo_banner_description">Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗ’єднайте до трьох карток для бекапу.</string>
|
||||
<string name="wallet_promo_banner_title">Відкрийте Tangem Wallet</string>
|
||||
<string name="wallet_settings_push_notifications_description">Отримуйте сповіщення про вхідні транзакції в гаманці та оновлення Tangem.</string>
|
||||
<string name="wallet_settings_push_notifications_title">Сповіщення про транзакції</string>
|
||||
<string name="wallet_settings_title">Налаштування гаманця</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
|
|
@ -1193,7 +1206,7 @@
|
|||
<string name="warning_express_approval_in_progress_message">Дозвіл обміну триває і незабаром буде завершено</string>
|
||||
<string name="warning_express_approval_in_progress_title">Затвердження в процесі</string>
|
||||
<string name="warning_express_dust_message">Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">У вашому списку немає доступних монет для обміну %s</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">У вашому портфелі немає токенів, на які можна обміняти %s. Будь ласка, додайте інший токен, щоб виконати обмін.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Немає доступних токенів для обміну</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Неможливо покрити комісію %s</string>
|
||||
|
|
@ -1257,8 +1270,23 @@
|
|||
<string name="warning_testnet_card_message">Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки.</string>
|
||||
<string name="warning_testnet_card_title">Лише для цілей тестування</string>
|
||||
<string name="warning_token_balance_not_updated">Баланс може бути застарілим. Оновіть сторінку.</string>
|
||||
<string name="warning_token_trustline_button_title">Відкрити Trustline</string>
|
||||
<string name="warning_token_trustline_subtitle">Відповідно до правил мережі, щоб поповнити цей токен, спочатку потрібно відкрити Trustline — це дозволить вашому гаманцю приймати та зберігати цей актив.</string>
|
||||
<string name="warning_token_trustline_title">Відкрийте Trustline</string>
|
||||
<string name="wc_alert_session_disconnected_description">Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect.</string>
|
||||
<string name="wc_alert_wrong_card_description">Обрана не вірна картка або кільце</string>
|
||||
<string name="wc_common_address">Адреса</string>
|
||||
<string name="wc_common_connect">Підключення</string>
|
||||
<string name="wc_connection_reqeust_can_view_balance">Переглянути баланс гаманця та активність</string>
|
||||
<string name="wc_connection_request">Запит на підключення</string>
|
||||
<string name="wc_contents">Вміст</string>
|
||||
<string name="wc_copy_data_button_text">Копіювати дані</string>
|
||||
<string name="wc_request_from">Запит від</string>
|
||||
<string name="wc_signature_type">Тип підпису</string>
|
||||
<string name="wc_transaction_info_to_title">До</string>
|
||||
<string name="wc_transaction_request">Запит транзакції</string>
|
||||
<string name="wc_transaction_request_title">Запит транзакції</string>
|
||||
<string name="wc_wallet_connect">Підключення гаманця</string>
|
||||
<string name="welcome_interrupted_backup_alert_discard">Відмовитися</string>
|
||||
<string name="welcome_interrupted_backup_alert_message">Ви не завершили резервне копіювання. Бажаєте продовжити?</string>
|
||||
<string name="welcome_interrupted_backup_alert_resume">Так, поновити</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
<string name="access_code_alert_skip_ok">Skip anyway</string>
|
||||
<string name="access_code_alert_skip_title">Access Code not set</string>
|
||||
<string name="access_code_check_title">Enter Access Code</string>
|
||||
<string name="access_code_check_warining_delete">Wrong access code. Your hot wallet will be deleted after %s more incorrect attempts.</string>
|
||||
<string name="access_code_check_warining_lock">Wrong access code. App will be locked with %s more input errors</string>
|
||||
<string name="access_code_check_warining_wait">Wrong access code.\nPlease wait %s seconds and try again.</string>
|
||||
<string name="access_code_confirm_description">Confirm your previously entered code to continue</string>
|
||||
<string name="access_code_confirm_title">Re-enter Access Code</string>
|
||||
<string name="access_code_create_description">Set a %s-digit Access Code to unlock your wallet.</string>
|
||||
|
|
@ -58,6 +61,9 @@
|
|||
<string name="app_settings_theme_mode_system">System default</string>
|
||||
<string name="app_settings_theme_selector_title">Theme</string>
|
||||
<string name="app_settings_title">App settings</string>
|
||||
<string name="auth_info_add_wallet_title">Add Wallet</string>
|
||||
<string name="auth_info_subtitle">Select a wallet to log in</string>
|
||||
<string name="auth_info_title">Welcome back!</string>
|
||||
<string name="backup_complete_description">You successfully backed up your wallet.</string>
|
||||
<string name="backup_complete_seed_description">These words can’t be recovered if lost. Make sure to keep it somewhere secure.</string>
|
||||
<string name="backup_complete_title">Backup Completed</string>
|
||||
|
|
@ -432,6 +438,7 @@
|
|||
<string name="hw_create_seed_description">Stay up to date with the latest features and news</string>
|
||||
<string name="hw_create_seed_title">Seed phrase backup</string>
|
||||
<string name="hw_create_title">Create Mobile Wallet</string>
|
||||
<string name="hw_mobile_wallet">Mobile Wallet</string>
|
||||
<string name="information_generated_with_ai">This information was generated with AI.\nTap here, if you find any errors.</string>
|
||||
<string name="initial_message_change_access_code_body">To change the access code tap the card or ring as shown above and do not remove until the end of the operation</string>
|
||||
<string name="initial_message_change_passcode_body">To change the passcode tap the card as shown above and do not remove until the end of the operation</string>
|
||||
|
|
@ -666,7 +673,7 @@
|
|||
<string name="onboarding_button_continue_wallet">Continue to my wallet</string>
|
||||
<string name="onboarding_button_finalize_backup">Finalize backup</string>
|
||||
<string name="onboarding_button_receive_crypto">Receive crypto</string>
|
||||
<string name="onboarding_button_scan_origin_card">Scan primary card</string>
|
||||
<string name="onboarding_button_scan_origin_card">Scan primary card or ring</string>
|
||||
<string name="onboarding_button_skip_backup">Skip for later</string>
|
||||
<string name="onboarding_button_what_does_it_mean">How does it work?</string>
|
||||
<string name="onboarding_create_wallet_body">Let\'s generate all the keys on your card or ring and create a secure wallet</string>
|
||||
|
|
@ -719,7 +726,7 @@
|
|||
<string name="onboarding_title_no_backup_cards">No backup devices</string>
|
||||
<string name="onboarding_title_notifications">Notifications</string>
|
||||
<string name="onboarding_title_one_backup_card">One backup device added</string>
|
||||
<string name="onboarding_title_scan_origin_card">Prepare your card</string>
|
||||
<string name="onboarding_title_scan_origin_card">Prepare your card or ring</string>
|
||||
<string name="onboarding_title_two_backup_cards">Two backup devices added</string>
|
||||
<string name="onboarding_top_up_body">To get started, simply top up the wallet with any amount</string>
|
||||
<string name="onboarding_top_up_body_no_account_error">To get started, simply top up the wallet with more than %1$s %2$s</string>
|
||||
|
|
@ -1360,8 +1367,8 @@
|
|||
<string name="warning_express_approval_in_progress_message">Swap approval is underway and will be completed shortly</string>
|
||||
<string name="warning_express_approval_in_progress_title">Approval in progress</string>
|
||||
<string name="warning_express_dust_message">The minimum swapping amount is %1$s. Please ensure that the remaining balance after the swap will not be less than %2$s.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">You do not have any %s exchangeable coins in your list</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">No available tokens to swap</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">You don’t have any tokens in your portfolio that %s can be swapped to. Please add another token to enable the exchange.</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">No compatible tokens added</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
|
||||
<string name="warning_express_notification_invalid_reserve_amount_title">The amount to receive must be at least %s</string>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ package com.tangem.core.ui.components.bottomsheets.modal
|
|||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -20,6 +17,7 @@ import com.tangem.core.ui.components.buttons.small.TangemIconButton
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
|
|
@ -30,6 +28,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun TangemModalBottomSheetTitle(
|
||||
modifier: Modifier = Modifier,
|
||||
title: TextReference? = null,
|
||||
subtitle: TextReference? = null,
|
||||
@DrawableRes startIconRes: Int? = null,
|
||||
onStartClick: (() -> Unit)? = null,
|
||||
@DrawableRes endIconRes: Int? = null,
|
||||
|
|
@ -49,13 +48,23 @@ fun TangemModalBottomSheetTitle(
|
|||
.align(Alignment.CenterStart),
|
||||
)
|
||||
}
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (endIconRes != null && onEndClick != null) {
|
||||
TangemIconButton(
|
||||
|
|
@ -79,6 +88,7 @@ private fun Preview_TangemModalBottomSheetTitle(
|
|||
TangemThemePreview {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = params.title,
|
||||
subtitle = params.subtitle,
|
||||
startIconRes = params.startIconRes,
|
||||
onStartClick = params.onStartClick,
|
||||
endIconRes = params.endIconRes,
|
||||
|
|
@ -90,6 +100,7 @@ private fun Preview_TangemModalBottomSheetTitle(
|
|||
|
||||
private data class TangemModalBottomSheetTitleData(
|
||||
val title: TextReference?,
|
||||
val subtitle: TextReference?,
|
||||
val startIconRes: Int?,
|
||||
val onStartClick: (() -> Unit)?,
|
||||
val endIconRes: Int?,
|
||||
|
|
@ -104,6 +115,7 @@ private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider<Tan
|
|||
onStartClick = {},
|
||||
endIconRes = null,
|
||||
onEndClick = null,
|
||||
subtitle = null,
|
||||
),
|
||||
TangemModalBottomSheetTitleData(
|
||||
title = resourceReference(R.string.wallet_title),
|
||||
|
|
@ -111,6 +123,7 @@ private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider<Tan
|
|||
onStartClick = null,
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = {},
|
||||
subtitle = null,
|
||||
),
|
||||
TangemModalBottomSheetTitleData(
|
||||
title = resourceReference(R.string.wallet_title),
|
||||
|
|
@ -118,6 +131,23 @@ private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider<Tan
|
|||
onStartClick = {},
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = {},
|
||||
subtitle = null,
|
||||
),
|
||||
TangemModalBottomSheetTitleData(
|
||||
title = resourceReference(R.string.wallet_title),
|
||||
subtitle = stringReference("Today • 2:37 PM"),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = {},
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = {},
|
||||
),
|
||||
TangemModalBottomSheetTitleData(
|
||||
title = null,
|
||||
subtitle = stringReference("Today • 2:37 PM"),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = {},
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = {},
|
||||
),
|
||||
TangemModalBottomSheetTitleData(
|
||||
title = null,
|
||||
|
|
@ -125,6 +155,7 @@ private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider<Tan
|
|||
onStartClick = {},
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = {},
|
||||
subtitle = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ private fun CellDecoration(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
|
|||
|
|
@ -5,26 +5,32 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.components.tooltip.TangemTooltip
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
|
||||
|
|
@ -51,6 +57,7 @@ fun InputRowEnter(
|
|||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: TextReference? = null,
|
||||
description: TextReference? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
isSingleLine: Boolean = false,
|
||||
|
|
@ -71,11 +78,32 @@ fun InputRowEnter(
|
|||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
if (description != null) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
SimpleTextField(
|
||||
value = text,
|
||||
onValueChange = onValueChange,
|
||||
|
|
@ -121,6 +149,7 @@ private fun InputRowEnterPreview(
|
|||
text = data.text,
|
||||
iconRes = data.iconRes,
|
||||
showDivider = data.showDivider,
|
||||
description = stringReference(""),
|
||||
onValueChange = {},
|
||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -8,6 +9,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -15,6 +17,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.components.tooltip.TangemTooltip
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -118,6 +121,7 @@ fun InputRowEnterInfoAmountV2(
|
|||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
info: TextReference? = null,
|
||||
description: TextReference? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -133,7 +137,9 @@ fun InputRowEnterInfoAmountV2(
|
|||
paddingValues = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
|
|
@ -141,12 +147,22 @@ fun InputRowEnterInfoAmountV2(
|
|||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
if (description != null) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
AmountTextField(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.core.ui.components.tooltip
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TangemTooltip(text: String, content: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier) {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp),
|
||||
state = tooltipState,
|
||||
modifier = modifier,
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
caretSize = DpSize(width = 14.dp, height = 8.dp),
|
||||
contentColor = TangemTheme.colors.text.primary2,
|
||||
containerColor = TangemTheme.colors.icon.secondary,
|
||||
content = {
|
||||
Text(
|
||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
content = {
|
||||
content(
|
||||
Modifier.clickableSingle(
|
||||
onClick = { coroutineScope.launch { tooltipState.show() } },
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TangemTooltip_Preview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(500.dp)
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.size(64.dp),
|
||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.",
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(64.dp),
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue