Updated on 2026-08-14
This commit is contained in:
commit
bfd903b43c
595 changed files with 12755 additions and 19837 deletions
|
|
@ -152,6 +152,7 @@ dependencies {
|
||||||
implementation(projects.domain.manageTokens)
|
implementation(projects.domain.manageTokens)
|
||||||
implementation(projects.domain.nft)
|
implementation(projects.domain.nft)
|
||||||
implementation(projects.domain.nft.models)
|
implementation(projects.domain.nft.models)
|
||||||
|
implementation(projects.domain.offramp)
|
||||||
implementation(projects.domain.onramp)
|
implementation(projects.domain.onramp)
|
||||||
implementation(projects.domain.promo)
|
implementation(projects.domain.promo)
|
||||||
implementation(projects.domain.promo.models)
|
implementation(projects.domain.promo.models)
|
||||||
|
|
@ -304,6 +305,8 @@ dependencies {
|
||||||
implementation(projects.features.tokenRecieve.impl)
|
implementation(projects.features.tokenRecieve.impl)
|
||||||
implementation(projects.features.yieldSupply.api)
|
implementation(projects.features.yieldSupply.api)
|
||||||
implementation(projects.features.yieldSupply.impl)
|
implementation(projects.features.yieldSupply.impl)
|
||||||
|
implementation(projects.features.approval.api)
|
||||||
|
implementation(projects.features.approval.impl)
|
||||||
|
|
||||||
/** AndroidX libraries */
|
/** AndroidX libraries */
|
||||||
implementation(deps.androidx.core.ktx)
|
implementation(deps.androidx.core.ktx)
|
||||||
|
|
|
||||||
|
|
@ -146,11 +146,8 @@ abstract class BaseTestCase : TestCase(
|
||||||
return ApplicationInjectionExecutionRule(
|
return ApplicationInjectionExecutionRule(
|
||||||
toggleStates = mapOf(
|
toggleStates = mapOf(
|
||||||
"SWAP_REDESIGN_ENABLED" to false,
|
"SWAP_REDESIGN_ENABLED" to false,
|
||||||
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
|
||||||
"HOT_WALLET_ENABLED" to true,
|
"HOT_WALLET_ENABLED" to true,
|
||||||
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
|
|
||||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||||
"FEED_ENABLED" to true,
|
|
||||||
"GASLESS_TRANSACTIONS_ENABLED" to true,
|
"GASLESS_TRANSACTIONS_ENABLED" to true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
package com.tangem.tests
|
package com.tangem.tests
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.extensions.SwipeDirection
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.swipeVertical
|
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
|
@ -22,30 +20,32 @@ class SendTest : BaseTestCase() {
|
||||||
@DisplayName("Send: check fee notification")
|
@DisplayName("Send: check fee notification")
|
||||||
@Test
|
@Test
|
||||||
fun checkFeeNotificationTest() {
|
fun checkFeeNotificationTest() {
|
||||||
val currencyName = "POL (ex-MATIC)"
|
val currencyName = "USDC"
|
||||||
val feeCurrencyName = "Ethereum"
|
val feeCurrencyName = "Solana"
|
||||||
val feeCurrencySymbol = "ETH"
|
val feeCurrencySymbol = "SOL"
|
||||||
val scenarioName = "eth_network_balance"
|
val balanceScenarioName = "solana_balance"
|
||||||
val scenarioState = "Empty"
|
val tokensScenarioName = "user_tokens_api"
|
||||||
|
val balanceState = "Empty"
|
||||||
|
val tokensState = "SolanaUSDC"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
resetWireMockScenarioState(scenarioName)
|
resetWireMockScenarioState(balanceScenarioName)
|
||||||
|
resetWireMockScenarioState(tokensScenarioName)
|
||||||
}
|
}
|
||||||
).run {
|
).run {
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
}
|
}
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Swipe up") {
|
|
||||||
swipeVertical(SwipeDirection.UP)
|
|
||||||
}
|
|
||||||
step("Click on token with name: $currencyName") {
|
step("Click on token with name: $currencyName") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class KusamaWarningsTest : BaseTestCase() {
|
||||||
private val tokenName = "Kusama"
|
private val tokenName = "Kusama"
|
||||||
private val amountToLeaveLessThanDeposit = "0.300333"
|
private val amountToLeaveLessThanDeposit = "0.300333"
|
||||||
private val amountToLeaveGreaterThanDeposit = "0.1"
|
private val amountToLeaveGreaterThanDeposit = "0.1"
|
||||||
private val depositAmount = "KSM 0.000333333333"
|
private val depositAmount = "KSM 0.000003333"
|
||||||
|
|
||||||
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
|
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
|
||||||
private val warningMessage = getResourceString(
|
private val warningMessage = getResourceString(
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class PolkadotWarningsTest : BaseTestCase() {
|
||||||
private val tokenName = "Polkadot"
|
private val tokenName = "Polkadot"
|
||||||
private val amountToLeaveLessThanDeposit = "1.299"
|
private val amountToLeaveLessThanDeposit = "1.299"
|
||||||
private val amountToLeaveGreaterThanDeposit = "0.2"
|
private val amountToLeaveGreaterThanDeposit = "0.2"
|
||||||
private val depositAmount = "DOT 1.00"
|
private val depositAmount = "DOT 0.01"
|
||||||
|
|
||||||
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
|
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
|
||||||
private val warningMessage = getResourceString(
|
private val warningMessage = getResourceString(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
||||||
import com.tangem.data.card.TransactionSignerFactory
|
import com.tangem.data.card.TransactionSignerFactory
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
|
|
@ -48,7 +48,6 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
@ -58,12 +57,12 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
interface ApplicationEntryPoint {
|
interface ApplicationEntryPoint {
|
||||||
|
|
||||||
fun getEnvironmentConfigStorage(): EnvironmentConfigStorage
|
|
||||||
|
|
||||||
fun getAppStateHolder(): AppStateHolder
|
fun getAppStateHolder(): AppStateHolder
|
||||||
|
|
||||||
fun getIssuersConfigStorage(): IssuersConfigStorage
|
fun getIssuersConfigStorage(): IssuersConfigStorage
|
||||||
|
|
||||||
|
fun getEnvironmentConfig(): EnvironmentConfig
|
||||||
|
|
||||||
fun getFeatureTogglesManager(): FeatureTogglesManager
|
fun getFeatureTogglesManager(): FeatureTogglesManager
|
||||||
|
|
||||||
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
|
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
|
||||||
|
|
@ -120,8 +119,6 @@ interface ApplicationEntryPoint {
|
||||||
|
|
||||||
fun getOnboardingRepository(): OnboardingRepository
|
fun getOnboardingRepository(): OnboardingRepository
|
||||||
|
|
||||||
fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
|
|
||||||
|
|
||||||
fun getExcludedBlockchains(): ExcludedBlockchains
|
fun getExcludedBlockchains(): ExcludedBlockchains
|
||||||
|
|
||||||
fun getAppLogsStore(): AppLogsStore
|
fun getAppLogsStore(): AppLogsStore
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
|
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
|
||||||
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
||||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
|
||||||
import com.tangem.features.tester.api.TesterMenuLauncher
|
import com.tangem.features.tester.api.TesterMenuLauncher
|
||||||
import com.tangem.google.GoogleServicesHelper
|
import com.tangem.google.GoogleServicesHelper
|
||||||
import com.tangem.operations.backup.BackupService
|
import com.tangem.operations.backup.BackupService
|
||||||
|
|
@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
@Inject
|
@Inject
|
||||||
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
|
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
|
||||||
|
|
||||||
@Inject
|
|
||||||
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
|
|
||||||
|
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
|
|
@ -73,23 +72,19 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.common.redux.appReducer
|
import com.tangem.tap.common.redux.appReducer
|
||||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||||
import com.tangem.tap.domain.tasks.product.DerivationsFinder
|
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import com.tangem.wallet.BuildConfig
|
import com.tangem.wallet.BuildConfig
|
||||||
import dagger.hilt.EntryPoints
|
import dagger.hilt.EntryPoints
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
lateinit var store: Store<AppState>
|
lateinit var store: Store<AppState>
|
||||||
|
|
||||||
val foregroundActivityObserver = ForegroundActivityObserver
|
val foregroundActivityObserver = ForegroundActivityObserver
|
||||||
internal lateinit var derivationsFinder: DerivationsFinder
|
|
||||||
|
|
||||||
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
||||||
|
|
||||||
|
|
@ -100,12 +95,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val appStateHolder: AppStateHolder
|
private val appStateHolder: AppStateHolder
|
||||||
get() = entryPoint.getAppStateHolder()
|
get() = entryPoint.getAppStateHolder()
|
||||||
|
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage
|
|
||||||
get() = entryPoint.getEnvironmentConfigStorage()
|
|
||||||
|
|
||||||
private val issuersConfigStorage: IssuersConfigStorage
|
private val issuersConfigStorage: IssuersConfigStorage
|
||||||
get() = entryPoint.getIssuersConfigStorage()
|
get() = entryPoint.getIssuersConfigStorage()
|
||||||
|
|
||||||
|
private val environmentConfig: EnvironmentConfig
|
||||||
|
get() = entryPoint.getEnvironmentConfig()
|
||||||
|
|
||||||
private val featureTogglesManager: FeatureTogglesManager
|
private val featureTogglesManager: FeatureTogglesManager
|
||||||
get() = entryPoint.getFeatureTogglesManager()
|
get() = entryPoint.getFeatureTogglesManager()
|
||||||
|
|
||||||
|
|
@ -190,9 +185,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val onboardingRepository: OnboardingRepository
|
private val onboardingRepository: OnboardingRepository
|
||||||
get() = entryPoint.getOnboardingRepository()
|
get() = entryPoint.getOnboardingRepository()
|
||||||
|
|
||||||
private val dispatchers: CoroutineDispatcherProvider
|
|
||||||
get() = entryPoint.getCoroutineDispatcherProvider()
|
|
||||||
|
|
||||||
private val excludedBlockchains: ExcludedBlockchains
|
private val excludedBlockchains: ExcludedBlockchains
|
||||||
get() = entryPoint.getExcludedBlockchains()
|
get() = entryPoint.getExcludedBlockchains()
|
||||||
|
|
||||||
|
|
@ -312,9 +304,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
Timber.i(excludedBlockchainsManager.toString())
|
Timber.i(excludedBlockchainsManager.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
initWithConfigDependency(environmentConfig = environmentConfig)
|
||||||
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
abTestsManager.init()
|
abTestsManager.init()
|
||||||
|
|
||||||
|
|
@ -348,15 +338,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
derivationsFinder = DerivationsFinder(
|
|
||||||
userTokensResponseStore = userTokensResponseStore,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
appStateHolder.mainStore = store
|
appStateHolder.mainStore = store
|
||||||
|
|
||||||
wcInitializeUseCase.init(
|
wcInitializeUseCase.init(
|
||||||
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
|
projectId = environmentConfig.walletConnectProjectId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,7 +372,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
shareManager = shareManager,
|
shareManager = shareManager,
|
||||||
appRouter = appRouter,
|
appRouter = appRouter,
|
||||||
transactionSignerFactory = transactionSignerFactory,
|
transactionSignerFactory = transactionSignerFactory,
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
|
||||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||||
onboardingRepository = onboardingRepository,
|
onboardingRepository = onboardingRepository,
|
||||||
excludedBlockchains = excludedBlockchains,
|
excludedBlockchains = excludedBlockchains,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware
|
||||||
import com.tangem.tap.features.details.redux.DetailsMiddleware
|
import com.tangem.tap.features.details.redux.DetailsMiddleware
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
||||||
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
|
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import org.rekotlin.Middleware
|
import org.rekotlin.Middleware
|
||||||
|
|
@ -29,7 +28,6 @@ data class AppState(
|
||||||
AccessCodeRequestPolicyMiddleware().middleware,
|
AccessCodeRequestPolicyMiddleware().middleware,
|
||||||
DaggerGraphMiddleware.daggerGraphMiddleware,
|
DaggerGraphMiddleware.daggerGraphMiddleware,
|
||||||
LegacyMiddleware.legacyMiddleware,
|
LegacyMiddleware.legacyMiddleware,
|
||||||
TradeCryptoMiddleware.middleware,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [OfframpRepository]
|
||||||
|
*
|
||||||
|
* @property sellService sell service for getting offramp URL
|
||||||
|
*/
|
||||||
|
internal class DefaultOfframpRepository(
|
||||||
|
private val sellService: SellService,
|
||||||
|
) : OfframpRepository {
|
||||||
|
|
||||||
|
override fun getOfframpUrl(
|
||||||
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
fiatCurrencyCode: String,
|
||||||
|
walletAddress: String,
|
||||||
|
): String? {
|
||||||
|
return sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.tap.di
|
package com.tangem.tap.di
|
||||||
|
|
||||||
import com.tangem.datasource.api.moonpay.MoonPayApi
|
import com.tangem.datasource.api.moonpay.MoonPayApi
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.card.ScanCardUseCase
|
import com.tangem.domain.card.ScanCardUseCase
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.exchange.RampStateManager
|
import com.tangem.domain.exchange.RampStateManager
|
||||||
|
|
@ -69,14 +69,14 @@ internal object ActivityModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideExchangeService(
|
fun provideExchangeService(
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
|
||||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||||
moonPayApi: MoonPayApi,
|
moonPayApi: MoonPayApi,
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
): SellService {
|
): SellService {
|
||||||
return MoonPayService(
|
return MoonPayService(
|
||||||
api = moonPayApi,
|
api = moonPayApi,
|
||||||
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
|
apiKey = environmentConfig.moonPayApiKey,
|
||||||
secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
|
secretKey = environmentConfig.moonPayApiSecretKey,
|
||||||
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
|
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.sdk.api.TangemSdkManager
|
import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||||
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
||||||
|
import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
|
||||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||||
|
|
@ -40,6 +41,7 @@ internal class TangemSdkManagerModule {
|
||||||
appFinisher: AppFinisher,
|
appFinisher: AppFinisher,
|
||||||
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
|
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): TangemSdkManager {
|
): TangemSdkManager {
|
||||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||||
|
|
@ -56,6 +58,7 @@ internal class TangemSdkManagerModule {
|
||||||
appFinisher = appFinisher,
|
appFinisher = appFinisher,
|
||||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||||
|
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.earn.repository.EarnRepository
|
import com.tangem.domain.earn.repository.EarnRepository
|
||||||
import com.tangem.domain.earn.usecase.*
|
import com.tangem.domain.earn.usecase.*
|
||||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -19,15 +19,15 @@ object EarnDomainModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideManageEarnNetworksUseCase(
|
fun provideGetEarnNetworksUseCase(
|
||||||
earnRepository: EarnRepository,
|
earnRepository: EarnRepository,
|
||||||
|
multiAccountListSupplier: MultiAccountListSupplier,
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
|
||||||
): GetEarnNetworksUseCase {
|
): GetEarnNetworksUseCase {
|
||||||
return GetEarnNetworksUseCase(
|
return GetEarnNetworksUseCase(
|
||||||
earnRepository = earnRepository,
|
earnRepository = earnRepository,
|
||||||
|
multiAccountListSupplier = multiAccountListSupplier,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
import com.tangem.domain.onramp.*
|
import com.tangem.domain.onramp.*
|
||||||
import com.tangem.domain.onramp.repositories.*
|
import com.tangem.domain.onramp.repositories.*
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
|
import com.tangem.tap.data.DefaultOfframpRepository
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -275,4 +279,16 @@ internal object OnrampDomainModule {
|
||||||
promoRepository = promoRepository,
|
promoRepository = promoRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOfframpRepository(sellService: SellService): OfframpRepository {
|
||||||
|
return DefaultOfframpRepository(sellService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
|
||||||
|
return GetOfframpUrlUseCase(offrampRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||||
import com.tangem.domain.networks.repository.NetworksRepository
|
import com.tangem.domain.networks.repository.NetworksRepository
|
||||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||||
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.quotes.QuotesRepository
|
import com.tangem.domain.quotes.QuotesRepository
|
||||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||||
|
|
@ -176,20 +177,6 @@ internal object TokensDomainModule {
|
||||||
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
|
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideToggleTokenListGroupingUseCase(
|
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
): ToggleTokenListGroupingUseCase {
|
|
||||||
return ToggleTokenListGroupingUseCase(dispatchers)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
|
|
||||||
return ToggleTokenListSortingUseCase(dispatchers)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideApplyTokenListSortingUseCase(
|
fun provideApplyTokenListSortingUseCase(
|
||||||
|
|
@ -378,6 +365,7 @@ internal object TokensDomainModule {
|
||||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||||
|
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
stakingIdFactory: StakingIdFactory,
|
stakingIdFactory: StakingIdFactory,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): WalletBalanceFetcher {
|
): WalletBalanceFetcher {
|
||||||
|
|
@ -388,6 +376,7 @@ internal object TokensDomainModule {
|
||||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||||
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
stakingIdFactory = stakingIdFactory,
|
stakingIdFactory = stakingIdFactory,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.sdk.api.visa.VisaCardActivationResponse
|
import com.tangem.sdk.api.visa.VisaCardActivationResponse
|
||||||
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
|
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
|
||||||
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
||||||
import com.tangem.tap.derivationsFinder
|
import com.tangem.tap.domain.tasks.product.*
|
||||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
|
|
||||||
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
|
|
||||||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
|
||||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
|
||||||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||||
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
|
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
|
||||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||||
|
|
@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager(
|
||||||
private val appFinisher: AppFinisher,
|
private val appFinisher: AppFinisher,
|
||||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
) : TangemSdkManager {
|
) : TangemSdkManager {
|
||||||
|
|
||||||
|
|
@ -172,7 +169,7 @@ internal class DefaultTangemSdkManager(
|
||||||
runTaskAsyncReturnOnMain(
|
runTaskAsyncReturnOnMain(
|
||||||
runnable = ScanProductTask(
|
runnable = ScanProductTask(
|
||||||
card = null,
|
card = null,
|
||||||
derivationsFinder = derivationsFinder,
|
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||||
visaCardScanHandler = visaCardScanHandler,
|
visaCardScanHandler = visaCardScanHandler,
|
||||||
visaCoroutineScope = this,
|
visaCoroutineScope = this,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.tangem.tap.domain.tasks.product
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||||
|
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||||
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
|
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||||
|
import com.tangem.data.wallets.derivations.BlockchainToDerive
|
||||||
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||||
|
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||||
|
import com.tangem.tap.features.demo.DemoHelper
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finder of blockchains to derive.
|
||||||
|
* Returns only saved, default or demo blockchains without any additional logic
|
||||||
|
* (no cardano/ethereum additions or unnecessary blockchain removals).
|
||||||
|
*/
|
||||||
|
class BlockchainToDeriveFinder @Inject constructor(
|
||||||
|
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun find(card: CardDTO): Set<BlockchainToDerive> {
|
||||||
|
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
|
||||||
|
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
|
||||||
|
|
||||||
|
val derivationStyle = card.derivationStyleProvider.getDerivationStyle()
|
||||||
|
|
||||||
|
val blockchains = getBlockchains(userWalletId).ifEmpty {
|
||||||
|
if (DemoHelper.isDemoCardId(card.cardId)) {
|
||||||
|
getDemoBlockchains(derivationStyle, card.cardId)
|
||||||
|
} else {
|
||||||
|
getDefaultBlockchains(derivationStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockchains
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getBlockchains(userWalletId: UserWalletId): Set<BlockchainToDerive> {
|
||||||
|
return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty()
|
||||||
|
.flatMap { accountDTO ->
|
||||||
|
accountDTO.tokens.orEmpty()
|
||||||
|
.filter { it.contractAddress == null }
|
||||||
|
}
|
||||||
|
.mapNotNull { coin ->
|
||||||
|
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
|
||||||
|
val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null
|
||||||
|
|
||||||
|
BlockchainToDerive(blockchain, derivationPath)
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set<BlockchainToDerive> {
|
||||||
|
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set<BlockchainToDerive> {
|
||||||
|
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||||
|
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
|
||||||
|
derivationStyle: DerivationStyle?,
|
||||||
|
): Set<BlockchainToDerive> {
|
||||||
|
return mapNotNullTo(hashSetOf()) { blockchain ->
|
||||||
|
val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
|
||||||
|
BlockchainToDerive(blockchain, derivationPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
package com.tangem.tap.domain.tasks.product
|
|
||||||
|
|
||||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
|
||||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
|
||||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
|
||||||
import com.tangem.crypto.hdWallet.DerivationPath
|
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
|
||||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
|
||||||
import com.tangem.tap.features.demo.DemoHelper
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
internal data class BlockchainToDerive(
|
|
||||||
val blockchain: Blockchain,
|
|
||||||
val derivationPath: DerivationPath?,
|
|
||||||
)
|
|
||||||
|
|
||||||
// FIXME: May be move to DI, currently unnecessary
|
|
||||||
internal class DerivationsFinder(
|
|
||||||
private val userTokensResponseStore: UserTokensResponseStore,
|
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun findBlockchainsToDerive(
|
|
||||||
card: CardDTO,
|
|
||||||
derivationStyleProvider: DerivationStyleProvider,
|
|
||||||
): Set<BlockchainToDerive> {
|
|
||||||
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
|
|
||||||
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
|
|
||||||
val derivationStyle = derivationStyleProvider.getDerivationStyle()
|
|
||||||
|
|
||||||
val blockchains = withContext(dispatchers.io) {
|
|
||||||
getBlockchains(userWalletId)
|
|
||||||
}.ifEmpty {
|
|
||||||
if (DemoHelper.isDemoCardId(card.cardId)) {
|
|
||||||
getDemoBlockchains(derivationStyle, card.cardId)
|
|
||||||
} else {
|
|
||||||
getDefaultBlockchains(derivationStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we should generate second key for cardano
|
|
||||||
// because cardano address generation for wallet2 requires keys from 2 derivations
|
|
||||||
// https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
|
|
||||||
blockchains.addSecondCardanoDerivationIfPresent()
|
|
||||||
|
|
||||||
if (card.settings.isHDWalletAllowed) {
|
|
||||||
blockchains.addEthereumBlockchains(derivationStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pay attention to this
|
|
||||||
if (!card.hasOldStyleDerivation) {
|
|
||||||
blockchains.removeUnnecessaryBlockchains()
|
|
||||||
}
|
|
||||||
|
|
||||||
return blockchains
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
|
|
||||||
val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens
|
|
||||||
?: return hashSetOf()
|
|
||||||
|
|
||||||
return responseTokens.asSequence()
|
|
||||||
.filter { it.contractAddress == null }
|
|
||||||
.mapNotNull { coin ->
|
|
||||||
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
|
|
||||||
val derivationPath = coin.derivationPath?.let(::DerivationPath)
|
|
||||||
|
|
||||||
BlockchainToDerive(blockchain, derivationPath)
|
|
||||||
}
|
|
||||||
.toMutableSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Move to user wallet config
|
|
||||||
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet<BlockchainToDerive> {
|
|
||||||
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Move to user wallet config
|
|
||||||
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet<BlockchainToDerive> {
|
|
||||||
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
|
||||||
|
|
||||||
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableSet<BlockchainToDerive>.addEthereumBlockchains(derivationStyle: DerivationStyle?) {
|
|
||||||
val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet)
|
|
||||||
.mapToBlockchainsWithDerivations(derivationStyle)
|
|
||||||
|
|
||||||
addAll(ethereumBlockchains)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableSet<BlockchainToDerive>.removeUnnecessaryBlockchains() {
|
|
||||||
val unnecessaryBlockchains = listOf(
|
|
||||||
Blockchain.BSC, Blockchain.BSCTestnet,
|
|
||||||
Blockchain.Polygon, Blockchain.PolygonTestnet,
|
|
||||||
Blockchain.RSK,
|
|
||||||
Blockchain.Fantom, Blockchain.FantomTestnet,
|
|
||||||
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
|
|
||||||
)
|
|
||||||
|
|
||||||
removeAll { it.blockchain in unnecessaryBlockchains }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableSet<BlockchainToDerive>.addSecondCardanoDerivationIfPresent() {
|
|
||||||
val cardanoDerivation = this
|
|
||||||
.firstOrNull { it.blockchain == Blockchain.Cardano }
|
|
||||||
?.derivationPath
|
|
||||||
?: return
|
|
||||||
|
|
||||||
val secondCardanoBlockchain = BlockchainToDerive(
|
|
||||||
blockchain = Blockchain.Cardano,
|
|
||||||
derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation),
|
|
||||||
)
|
|
||||||
|
|
||||||
add(secondCardanoBlockchain)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
|
|
||||||
derivationStyle: DerivationStyle?,
|
|
||||||
): MutableSet<BlockchainToDerive> {
|
|
||||||
return mapTo(hashSetOf()) { blockchain ->
|
|
||||||
BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv
|
||||||
import com.tangem.common.tlv.TlvDecoder
|
import com.tangem.common.tlv.TlvDecoder
|
||||||
import com.tangem.crypto.CryptoUtils
|
import com.tangem.crypto.CryptoUtils
|
||||||
import com.tangem.crypto.hdWallet.DerivationPath
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||||
import com.tangem.domain.card.common.TwinsHelper
|
import com.tangem.domain.card.common.TwinsHelper
|
||||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
|
||||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||||
import com.tangem.domain.card.configs.CardConfig
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
|
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
|
||||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
|
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
|
||||||
|
|
@ -45,11 +43,10 @@ import com.tangem.tap.scope
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
internal class ScanProductTask(
|
internal class ScanProductTask(
|
||||||
private val card: Card?,
|
private val card: Card?,
|
||||||
private val derivationsFinder: DerivationsFinder?,
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||||
private val visaCoroutineScope: CoroutineScope?,
|
private val visaCoroutineScope: CoroutineScope?,
|
||||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||||
|
|
@ -81,7 +78,7 @@ internal class ScanProductTask(
|
||||||
readVisaCard(
|
readVisaCard(
|
||||||
session = session,
|
session = session,
|
||||||
cardDto = cardDto,
|
cardDto = cardDto,
|
||||||
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
|
scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder),
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -89,7 +86,7 @@ internal class ScanProductTask(
|
||||||
|
|
||||||
val commandProcessor = when {
|
val commandProcessor = when {
|
||||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||||
else -> ScanWalletProcessor(derivationsFinder)
|
else -> ScanWalletProcessor(blockchainToDeriveFinder)
|
||||||
}
|
}
|
||||||
commandProcessor.proceed(cardDto, session) { processorResult ->
|
commandProcessor.proceed(cardDto, session) { processorResult ->
|
||||||
when (processorResult) {
|
when (processorResult) {
|
||||||
|
|
@ -170,7 +167,7 @@ internal class ScanProductTask(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ScanWalletProcessor(
|
private class ScanWalletProcessor(
|
||||||
private val derivationsFinder: DerivationsFinder?,
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||||
) : ProductCommandProcessor<ScanResponse> {
|
) : ProductCommandProcessor<ScanResponse> {
|
||||||
|
|
||||||
var primaryCard: PrimaryCard? = null
|
var primaryCard: PrimaryCard? = null
|
||||||
|
|
@ -293,7 +290,6 @@ private class ScanWalletProcessor(
|
||||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val productType = getWalletProductType(card)
|
val productType = getWalletProductType(card)
|
||||||
val config = CardConfig.createConfig(card)
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val scanResponse = ScanResponse(
|
val scanResponse = ScanResponse(
|
||||||
card = card,
|
card = card,
|
||||||
|
|
@ -301,8 +297,7 @@ private class ScanWalletProcessor(
|
||||||
walletData = session.environment.walletData,
|
walletData = session.environment.walletData,
|
||||||
primaryCard = primaryCard,
|
primaryCard = primaryCard,
|
||||||
)
|
)
|
||||||
val derivations =
|
val derivations = collectDerivations(card, scanResponse)
|
||||||
collectDerivations(card, config, scanResponse.derivationStyleProvider)
|
|
||||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
||||||
callback(CompletionResult.Success(scanResponse))
|
callback(CompletionResult.Success(scanResponse))
|
||||||
return@launch
|
return@launch
|
||||||
|
|
@ -332,32 +327,13 @@ private class ScanWalletProcessor(
|
||||||
|
|
||||||
private suspend fun collectDerivations(
|
private suspend fun collectDerivations(
|
||||||
card: CardDTO,
|
card: CardDTO,
|
||||||
config: CardConfig,
|
scanResponse: ScanResponse,
|
||||||
derivationStyleProvider: DerivationStyleProvider,
|
|
||||||
): Map<ByteArrayKey, List<DerivationPath>> {
|
): Map<ByteArrayKey, List<DerivationPath>> {
|
||||||
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
val blockchains = blockchainToDeriveFinder
|
||||||
val blockchains = derivationsFinder
|
?.find(card)
|
||||||
?.findBlockchainsToDerive(card, derivationStyleProvider)
|
?: return emptyMap()
|
||||||
?: return derivations
|
|
||||||
|
|
||||||
blockchains.forEach { blockchain ->
|
return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains)
|
||||||
val curve = config.primaryCurve(blockchain.blockchain)
|
|
||||||
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
|
|
||||||
if (wallet.chainCode == null) return@forEach
|
|
||||||
|
|
||||||
val key = wallet.publicKey.toMapKey()
|
|
||||||
val path = blockchain.derivationPath
|
|
||||||
if (path != null) {
|
|
||||||
val addedDerivations = derivations[key]
|
|
||||||
if (addedDerivations != null) {
|
|
||||||
derivations[key] = addedDerivations + path
|
|
||||||
} else {
|
|
||||||
derivations[key] = listOf(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return derivations
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||||
|
|
||||||
internal class DefaultTokensFeatureToggles(
|
internal class DefaultTokensFeatureToggles(
|
||||||
@Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager,
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
) : TokensFeatureToggles
|
) : TokensFeatureToggles {
|
||||||
|
|
||||||
|
override val isMultiAddressUtxoEnabled: Boolean
|
||||||
|
get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED")
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ class FinalizeTwinTask(
|
||||||
is CompletionResult.Success ->
|
is CompletionResult.Success ->
|
||||||
ScanProductTask(
|
ScanProductTask(
|
||||||
card = readResult.data,
|
card = readResult.data,
|
||||||
derivationsFinder = null,
|
blockchainToDeriveFinder = null,
|
||||||
visaCardScanHandler = null,
|
visaCardScanHandler = null,
|
||||||
visaCoroutineScope = null,
|
visaCoroutineScope = null,
|
||||||
shouldCheckIsAlreadyActivated = false,
|
shouldCheckIsAlreadyActivated = false,
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository(
|
||||||
.map { wallets.updateWith(it) }
|
.map { wallets.updateWith(it) }
|
||||||
}
|
}
|
||||||
.doOnSuccess { loadedWallets ->
|
.doOnSuccess { loadedWallets ->
|
||||||
userWallets.update { toUpdate ->
|
userWallets.update { _ ->
|
||||||
val selectedUserWalletId = selectedUserWalletRepository.get()
|
val selectedUserWalletId = selectedUserWalletRepository.get()
|
||||||
selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId }
|
selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId }
|
||||||
?: loadedWallets.firstOrNull()?.also {
|
?: loadedWallets.firstOrNull()?.also {
|
||||||
|
|
@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod")
|
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||||
override suspend fun unlock(
|
override suspend fun unlock(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
unlockMethod: UserWalletsListRepository.UnlockMethod,
|
unlockMethod: UserWalletsListRepository.UnlockMethod,
|
||||||
|
|
@ -315,7 +315,13 @@ internal class DefaultUserWalletsListRepository(
|
||||||
|
|
||||||
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
||||||
.doOnSuccess { sensitiveInfo ->
|
.doOnSuccess { sensitiveInfo ->
|
||||||
updateWallets { it?.updateWith(sensitiveInfo) }
|
updateWallets { wallets ->
|
||||||
|
// It is necessary to update derivations because when scanning we obtain the missing keys
|
||||||
|
wallets?.updateWith(
|
||||||
|
walletIdToSensitiveInformation = sensitiveInfo,
|
||||||
|
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
|
||||||
|
)
|
||||||
|
}
|
||||||
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
|
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
|
||||||
}
|
}
|
||||||
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
|
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.tangem.tap.domain.userWalletList.utils
|
package com.tangem.tap.domain.userWalletList.utils
|
||||||
|
|
||||||
|
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||||
|
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||||
|
|
||||||
|
|
@ -72,7 +74,10 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
|
||||||
return this.map { it.toUserWallet() }
|
return this.map { it.toUserWallet() }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
|
internal fun UserWallet.updateWith(
|
||||||
|
sensitiveInformation: UserWalletSensitiveInformation,
|
||||||
|
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
|
||||||
|
): UserWallet {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is UserWallet.Cold -> {
|
is UserWallet.Cold -> {
|
||||||
copy(
|
copy(
|
||||||
|
|
@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
|
||||||
card = scanResponse.card.copy(
|
card = scanResponse.card.copy(
|
||||||
wallets = requireNotNull(sensitiveInformation.wallets),
|
wallets = requireNotNull(sensitiveInformation.wallets),
|
||||||
),
|
),
|
||||||
|
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
|
||||||
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
|
||||||
|
|
||||||
internal fun List<UserWallet>.updateWith(
|
internal fun List<UserWallet>.updateWith(
|
||||||
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
||||||
|
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
|
||||||
): List<UserWallet> {
|
): List<UserWallet> {
|
||||||
return if (walletIdToSensitiveInformation.isEmpty()) {
|
return if (walletIdToSensitiveInformation.isEmpty()) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this.map { wallet ->
|
this.map { wallet ->
|
||||||
walletIdToSensitiveInformation[wallet.walletId]
|
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
|
||||||
?.let(wallet::updateWith)
|
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
|
||||||
?: wallet
|
|
||||||
|
if (sensitiveInformation != null) {
|
||||||
|
wallet.updateWith(sensitiveInformation, derivedKeys)
|
||||||
|
} else {
|
||||||
|
wallet
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ internal class CardSettingsModel @Inject constructor(
|
||||||
val card = scanResponse.card
|
val card = scanResponse.card
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
|
val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true
|
||||||
store.dispatchNavigationAction {
|
store.dispatchNavigationAction {
|
||||||
push(
|
push(
|
||||||
route = AppRoute.ResetToFactory(
|
route = AppRoute.ResetToFactory(
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package com.tangem.tap.features.wallet.redux.middlewares
|
|
||||||
|
|
||||||
import com.tangem.common.routing.AppRouter
|
|
||||||
import com.tangem.core.analytics.Analytics
|
|
||||||
import com.tangem.domain.models.network.NetworkAddress
|
|
||||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
|
||||||
import com.tangem.tap.common.analytics.events.Token
|
|
||||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
|
||||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
|
||||||
import com.tangem.tap.common.extensions.inject
|
|
||||||
import com.tangem.tap.common.redux.AppState
|
|
||||||
import com.tangem.tap.features.demo.DemoHelper
|
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
|
||||||
import com.tangem.tap.store
|
|
||||||
import org.rekotlin.Middleware
|
|
||||||
|
|
||||||
@Deprecated("Will be removed soon")
|
|
||||||
object TradeCryptoMiddleware {
|
|
||||||
|
|
||||||
val middleware: Middleware<AppState> = { _, appState ->
|
|
||||||
{ nextDispatch ->
|
|
||||||
{ action ->
|
|
||||||
if (action is TradeCryptoAction) {
|
|
||||||
handle(appState, action)
|
|
||||||
}
|
|
||||||
nextDispatch(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
|
||||||
if (DemoHelper.tryHandle(state)) return
|
|
||||||
|
|
||||||
when (action) {
|
|
||||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
|
||||||
is TradeCryptoAction.Sell -> proceedSellAction(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun proceedSellAction(action: TradeCryptoAction.Sell) {
|
|
||||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
|
||||||
?.defaultAddress
|
|
||||||
?.let(NetworkAddress.Address::value)
|
|
||||||
?: return
|
|
||||||
val currency = action.cryptoCurrencyStatus.currency
|
|
||||||
|
|
||||||
store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl(
|
|
||||||
cryptoCurrency = currency,
|
|
||||||
fiatCurrencyName = action.appCurrencyCode,
|
|
||||||
walletAddress = networkAddress,
|
|
||||||
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
|
|
||||||
)?.let { url ->
|
|
||||||
store.dispatchOpenUrl(url)
|
|
||||||
Analytics.send(Token.Withdraw.ScreenOpened())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openReceiptUrl(transactionId: String) {
|
|
||||||
store.dispatchNavigationAction(AppRouter::pop)
|
|
||||||
|
|
||||||
val sellService = store.inject(DaggerGraphState::appStateHolder).sellService
|
|
||||||
sellService?.getSellCryptoReceiptUrl(transactionId = transactionId)
|
|
||||||
?.let(store::dispatchOpenUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.network.auth
|
||||||
import com.tangem.common.extensions.toHexString
|
import com.tangem.common.extensions.toHexString
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
|
|
@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend
|
||||||
|
|
||||||
internal class DefaultAuthProvider(
|
internal class DefaultAuthProvider(
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) : AuthProvider {
|
) : AuthProvider {
|
||||||
|
|
||||||
override suspend fun getCardPublicKey(): String {
|
override suspend fun getCardPublicKey(): String {
|
||||||
|
|
@ -47,11 +47,11 @@ internal class DefaultAuthProvider(
|
||||||
ApiEnvironment.DEV,
|
ApiEnvironment.DEV,
|
||||||
ApiEnvironment.DEV_2,
|
ApiEnvironment.DEV_2,
|
||||||
ApiEnvironment.DEV_3,
|
ApiEnvironment.DEV_3,
|
||||||
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
|
-> environmentConfig.tangemApiKeyDev
|
||||||
ApiEnvironment.STAGE_2,
|
ApiEnvironment.STAGE_2,
|
||||||
ApiEnvironment.STAGE,
|
ApiEnvironment.STAGE,
|
||||||
-> environmentConfigStorage.getConfigSync().tangemApiKeyStage
|
-> environmentConfig.tangemApiKeyStage
|
||||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
|
ApiEnvironment.PROD -> environmentConfig.tangemApiKey
|
||||||
} ?: error("No tangem tech api config provided")
|
} ?: error("No tangem tech api config provided")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,8 +60,8 @@ internal class DefaultAuthProvider(
|
||||||
return ProviderSuspend {
|
return ProviderSuspend {
|
||||||
when (apiEnvironment.invoke()) {
|
when (apiEnvironment.invoke()) {
|
||||||
ApiEnvironment.DEV,
|
ApiEnvironment.DEV,
|
||||||
-> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev
|
-> environmentConfig.gaslessTxApiKeyDev
|
||||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey
|
ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey
|
||||||
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
|
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
|
||||||
} ?: error("No gasless tx api config provided")
|
} ?: error("No gasless tx api config provided")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package com.tangem.tap.network.auth
|
package com.tangem.tap.network.auth
|
||||||
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
|
|
||||||
internal class DefaultP2PEthPoolAuthProvider(
|
internal class DefaultP2PEthPoolAuthProvider(
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) : P2PEthPoolAuthProvider {
|
) : P2PEthPoolAuthProvider {
|
||||||
|
|
||||||
override fun getApiKey(): String {
|
override fun getApiKey(): String {
|
||||||
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
val keys = environmentConfig.p2pApiKey
|
||||||
?: error("No P2P api keys provided")
|
?: error("No P2P api keys provided")
|
||||||
|
|
||||||
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.tangem.tap.network.auth
|
package com.tangem.tap.network.auth
|
||||||
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||||
|
|
||||||
internal class DefaultStakeKitAuthProvider(
|
internal class DefaultStakeKitAuthProvider(
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) : StakeKitAuthProvider {
|
) : StakeKitAuthProvider {
|
||||||
|
|
||||||
override fun getApiKey(): String {
|
override fun getApiKey(): String {
|
||||||
return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided")
|
return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.tap.network.auth.di
|
package com.tangem.tap.network.auth.di
|
||||||
|
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
|
|
@ -22,11 +22,11 @@ internal class AuthModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAuthProvider(
|
fun provideAuthProvider(
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfig: EnvironmentConfig,
|
||||||
): AuthProvider {
|
): AuthProvider {
|
||||||
return DefaultAuthProvider(
|
return DefaultAuthProvider(
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfig = environmentConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,14 +38,14 @@ internal class AuthModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider {
|
fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider {
|
||||||
return DefaultStakeKitAuthProvider(environmentConfigStorage)
|
return DefaultStakeKitAuthProvider(environmentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider {
|
fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider {
|
||||||
return DefaultP2PEthPoolAuthProvider(environmentConfigStorage)
|
return DefaultP2PEthPoolAuthProvider(environmentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,4 @@ interface SellService {
|
||||||
walletAddress: String,
|
walletAddress: String,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
): String?
|
): String?
|
||||||
|
|
||||||
fun getSellCryptoReceiptUrl(transactionId: String): String?
|
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency
|
||||||
import com.tangem.tap.network.exchangeServices.SellService
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
|
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
|
||||||
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
|
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
|
||||||
import com.tangem.utils.Provider
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
class MoonPayService(
|
class MoonPayService(
|
||||||
private val api: MoonPayApi,
|
private val api: MoonPayApi,
|
||||||
private val apiKeyProvider: Provider<String>,
|
private val apiKey: String,
|
||||||
private val secretKeyProvider: Provider<String>,
|
private val secretKey: String,
|
||||||
private val userWalletProvider: () -> UserWallet?,
|
private val userWalletProvider: () -> UserWallet?,
|
||||||
) : SellService {
|
) : SellService {
|
||||||
|
|
||||||
|
|
@ -47,18 +46,18 @@ class MoonPayService(
|
||||||
_initializationStatus.value = lceLoading()
|
_initializationStatus.value = lceLoading()
|
||||||
|
|
||||||
performRequest {
|
performRequest {
|
||||||
val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
|
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
|
||||||
is Result.Failure -> {
|
is Result.Failure -> {
|
||||||
Timber.e("Failed to load user status", result.error)
|
Timber.e(result.error, "Failed to load user status")
|
||||||
_initializationStatus.value = result.error.lceError()
|
_initializationStatus.value = result.error.lceError()
|
||||||
return@performRequest
|
return@performRequest
|
||||||
}
|
}
|
||||||
is Result.Success -> result.data
|
is Result.Success -> result.data
|
||||||
}
|
}
|
||||||
|
|
||||||
val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
|
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
|
||||||
is Result.Failure -> {
|
is Result.Failure -> {
|
||||||
Timber.e("Failed to load currencies", result.error)
|
Timber.e(result.error, "Failed to load currencies")
|
||||||
_initializationStatus.value = result.error.lceError()
|
_initializationStatus.value = result.error.lceError()
|
||||||
return@performRequest
|
return@performRequest
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +162,7 @@ class MoonPayService(
|
||||||
val uri = Uri.Builder()
|
val uri = Uri.Builder()
|
||||||
.scheme(SCHEME)
|
.scheme(SCHEME)
|
||||||
.authority(URL_SELL)
|
.authority(URL_SELL)
|
||||||
.appendQueryParameter("apiKey", apiKeyProvider())
|
.appendQueryParameter("apiKey", apiKey)
|
||||||
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
|
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
|
||||||
.appendQueryParameter("refundWalletAddress", walletAddress)
|
.appendQueryParameter("refundWalletAddress", walletAddress)
|
||||||
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
|
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
|
||||||
|
|
@ -177,17 +176,9 @@ class MoonPayService(
|
||||||
return uri.build().toString()
|
return uri.build().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSellCryptoReceiptUrl(transactionId: String): String {
|
|
||||||
return Uri.Builder()
|
|
||||||
.scheme(SCHEME)
|
|
||||||
.authority(URL_SELL)
|
|
||||||
.appendPath("transaction_receipt")
|
|
||||||
.appendQueryParameter("transactionId", transactionId).build().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSignature(data: String): String {
|
private fun createSignature(data: String): String {
|
||||||
val sha256Hmac = Mac.getInstance("HmacSHA256")
|
val sha256Hmac = Mac.getInstance("HmacSHA256")
|
||||||
val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
|
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
|
||||||
sha256Hmac.init(secretKey)
|
sha256Hmac.init(secretKey)
|
||||||
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
|
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
|
||||||
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
|
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||||
import com.tangem.data.card.TransactionSignerFactory
|
import com.tangem.data.card.TransactionSignerFactory
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
|
|
@ -63,7 +62,6 @@ data class DaggerGraphState(
|
||||||
val shareManager: ShareManager? = null,
|
val shareManager: ShareManager? = null,
|
||||||
val appRouter: AppRouter? = null,
|
val appRouter: AppRouter? = null,
|
||||||
val transactionSignerFactory: TransactionSignerFactory? = null,
|
val transactionSignerFactory: TransactionSignerFactory? = null,
|
||||||
val environmentConfigStorage: EnvironmentConfigStorage? = null,
|
|
||||||
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
|
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
|
||||||
val onboardingRepository: OnboardingRepository? = null,
|
val onboardingRepository: OnboardingRepository? = null,
|
||||||
val excludedBlockchains: ExcludedBlockchains? = null,
|
val excludedBlockchains: ExcludedBlockchains? = null,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import com.tangem.features.details.component.DetailsComponent
|
||||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||||
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
|
||||||
import com.tangem.features.home.api.HomeComponent
|
import com.tangem.features.home.api.HomeComponent
|
||||||
import com.tangem.features.hotwallet.*
|
import com.tangem.features.hotwallet.*
|
||||||
import com.tangem.features.kyc.KycComponent
|
import com.tangem.features.kyc.KycComponent
|
||||||
|
|
@ -26,7 +25,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
|
||||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||||
import com.tangem.features.nft.component.NFTComponent
|
import com.tangem.features.nft.component.NFTComponent
|
||||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||||
|
|
@ -67,7 +65,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
|
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
|
||||||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||||
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
|
|
||||||
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
|
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
|
||||||
private val onrampComponentFactory: OnrampComponent.Factory,
|
private val onrampComponentFactory: OnrampComponent.Factory,
|
||||||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||||
|
|
@ -117,7 +114,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val kycComponentFactory: KycComponent.Factory,
|
private val kycComponentFactory: KycComponent.Factory,
|
||||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||||
private val feedFeatureToggle: FeedFeatureToggle,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
|
|
@ -193,39 +189,21 @@ internal class ChildFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppRoute.MarketsTokenDetails -> {
|
is AppRoute.MarketsTokenDetails -> {
|
||||||
if (feedFeatureToggle.isFeedEnabled) {
|
createComponentChild(
|
||||||
createComponentChild(
|
context = context,
|
||||||
context = context,
|
params = FeedEntryRoute.MarketTokenDetails(
|
||||||
params = FeedEntryRoute.MarketTokenDetails(
|
token = route.token,
|
||||||
token = route.token,
|
appCurrency = route.appCurrency,
|
||||||
appCurrency = route.appCurrency,
|
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
analyticsParams = route.analyticsParams?.let { params ->
|
||||||
analyticsParams = route.analyticsParams?.let { params ->
|
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
||||||
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
blockchain = params.blockchain,
|
||||||
blockchain = params.blockchain,
|
source = params.source,
|
||||||
source = params.source,
|
)
|
||||||
)
|
},
|
||||||
},
|
),
|
||||||
),
|
componentFactory = feedEntryComponentFactory,
|
||||||
componentFactory = feedEntryComponentFactory,
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
createComponentChild(
|
|
||||||
context = context,
|
|
||||||
params = MarketsTokenDetailsComponent.Params(
|
|
||||||
token = route.token,
|
|
||||||
appCurrency = route.appCurrency,
|
|
||||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
|
||||||
analyticsParams = route.analyticsParams?.let { params ->
|
|
||||||
MarketsTokenDetailsComponent.AnalyticsParams(
|
|
||||||
blockchain = params.blockchain,
|
|
||||||
source = params.source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
componentFactory = marketsTokenDetailsComponentFactory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AppRoute.Onramp -> {
|
is AppRoute.Onramp -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme
|
||||||
import com.tangem.data.card.sdk.CardSdkProvider
|
import com.tangem.data.card.sdk.CardSdkProvider
|
||||||
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
|
||||||
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
||||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||||
|
|
@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
private val promoDeepLink: PromoDeeplinkHandler.Factory,
|
private val promoDeepLink: PromoDeeplinkHandler.Factory,
|
||||||
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
|
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
|
||||||
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
|
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
|
||||||
private val feedFeatureToggle: FeedFeatureToggle,
|
|
||||||
) {
|
) {
|
||||||
private val permittedAppRoute = MutableStateFlow(false)
|
private val permittedAppRoute = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
onboardVisaDeepLink.create(deeplinkUri)
|
onboardVisaDeepLink.create(deeplinkUri)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> {
|
deeplinkUri.path?.startsWith("/news") == true -> {
|
||||||
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
|
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
|
import io.mockk.*
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class DefaultOfframpRepositoryTest {
|
||||||
|
|
||||||
|
private val sellService: SellService = mockk()
|
||||||
|
private val repository = DefaultOfframpRepository(sellService)
|
||||||
|
|
||||||
|
private val cryptoCurrency: CryptoCurrency = mockk()
|
||||||
|
private val fiatCurrencyCode = "USD"
|
||||||
|
private val walletAddress = "0x1234567890abcdef"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockkObject(MutableAppThemeModeHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(sellService)
|
||||||
|
unmockkObject(MutableAppThemeModeHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return url when sellService returns url with light theme`() {
|
||||||
|
// Arrange
|
||||||
|
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light"
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
} returns expectedUrl
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(expectedUrl)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return url when sellService returns url with dark theme`() {
|
||||||
|
// Arrange
|
||||||
|
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark"
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns true
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = true,
|
||||||
|
)
|
||||||
|
} returns expectedUrl
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(expectedUrl)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return null when sellService returns null`() {
|
||||||
|
// Arrange
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isNull()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
package com.tangem.tap.domain.tasks.product
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||||
|
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||||
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
|
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||||
|
import com.tangem.data.wallets.derivations.BlockchainToDerive
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||||
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import io.mockk.*
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
class BlockchainToDeriveFinderTest {
|
||||||
|
|
||||||
|
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
|
||||||
|
private val finder = BlockchainToDeriveFinder(
|
||||||
|
walletAccountsFetcher = walletAccountsFetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(walletAccountsFetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN card is not HD wallet THEN return empty set`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = mockk<CardDTO> {
|
||||||
|
every { this@mockk.settings.isHDWalletAllowed } returns false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Truth.assertThat(actual).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN card has empty wallets THEN return empty set`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = mockk<CardDTO> {
|
||||||
|
every { this@mockk.settings.isHDWalletAllowed } returns true
|
||||||
|
every { this@mockk.wallets } returns emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Truth.assertThat(actual).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = createCardDTO()
|
||||||
|
|
||||||
|
val response = createResponse(Blockchain.Bitcoin)
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = setOf(
|
||||||
|
createExpected(Blockchain.Bitcoin),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val demoCardId = "AC01000000045754"
|
||||||
|
val card = createCardDTO(cardId = demoCardId)
|
||||||
|
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = setOf(
|
||||||
|
createExpected(Blockchain.Bitcoin),
|
||||||
|
createExpected(Blockchain.Ethereum),
|
||||||
|
createExpected(Blockchain.Dogecoin),
|
||||||
|
createExpected(Blockchain.Solana),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val demoCardId = "DE00"
|
||||||
|
val card = createCardDTO(cardId = demoCardId)
|
||||||
|
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = setOf(
|
||||||
|
createExpected(Blockchain.Bitcoin),
|
||||||
|
createExpected(Blockchain.Ethereum),
|
||||||
|
createExpected(Blockchain.Dogecoin),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN empty store THEN return default blockchains`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = createCardDTO()
|
||||||
|
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = setOf(
|
||||||
|
createExpected(Blockchain.Bitcoin),
|
||||||
|
createExpected(Blockchain.Ethereum),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN saved cardano THEN return only cardano`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = createCardDTO()
|
||||||
|
|
||||||
|
val response = createResponse(Blockchain.Cardano)
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = setOf(
|
||||||
|
createExpected(Blockchain.Cardano),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val card = createCardDTO()
|
||||||
|
|
||||||
|
val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon)
|
||||||
|
|
||||||
|
val response = createResponse(*blockchains.toTypedArray())
|
||||||
|
|
||||||
|
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = finder.find(card)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = blockchains.mapTo(hashSetOf(), ::createExpected)
|
||||||
|
|
||||||
|
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO {
|
||||||
|
val wallet = mockk<CardDTO.Wallet> {
|
||||||
|
every { this@mockk.publicKey } returns byteArrayOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockk<CardDTO> {
|
||||||
|
every { this@mockk.cardId } returns cardId
|
||||||
|
every { this@mockk.batchId } returns batchId
|
||||||
|
every { this@mockk.settings.isHDWalletAllowed } returns true
|
||||||
|
every { this@mockk.settings.isKeysImportAllowed } returns true
|
||||||
|
every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion(
|
||||||
|
major = 6,
|
||||||
|
minor = 33,
|
||||||
|
patch = 0,
|
||||||
|
type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release,
|
||||||
|
)
|
||||||
|
every { this@mockk.wallets } returns listOf(wallet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse {
|
||||||
|
val tokens = blockchains.map { blockchain ->
|
||||||
|
mockk<UserTokensResponse.Token> {
|
||||||
|
every { this@mockk.networkId } returns blockchain.toNetworkId()
|
||||||
|
every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath
|
||||||
|
every { this@mockk.contractAddress } returns null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val account = mockk<WalletAccountDTO> {
|
||||||
|
every { this@mockk.tokens } returns tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockk {
|
||||||
|
every { this@mockk.accounts } returns listOf(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createExpected(
|
||||||
|
blockchain: Blockchain,
|
||||||
|
derivationPath: DerivationPath = blockchain.getDerivationPath(),
|
||||||
|
): BlockchainToDerive {
|
||||||
|
return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Blockchain.getDerivationPath(): DerivationPath {
|
||||||
|
return derivationPath(DerivationStyle.V3)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
// for byteArrayOf(0)
|
||||||
|
val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.data.card.sdk.CardSdkProvider
|
import com.tangem.data.card.sdk.CardSdkProvider
|
||||||
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
|
||||||
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
|
||||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||||
|
|
@ -87,7 +86,6 @@ class DeepLinkFactoryTest {
|
||||||
private val newsDeeplink = mockk<NewsDetailsDeepLinkHandler.Factory>(relaxed = true) {
|
private val newsDeeplink = mockk<NewsDetailsDeepLinkHandler.Factory>(relaxed = true) {
|
||||||
every { create(any(), any()) } returns mockk()
|
every { create(any(), any()) } returns mockk()
|
||||||
}
|
}
|
||||||
private val feedFeatureToggle = mockk<FeedFeatureToggle>()
|
|
||||||
|
|
||||||
private val mockedUri = mockk<Uri>(relaxed = true)
|
private val mockedUri = mockk<Uri>(relaxed = true)
|
||||||
private val isFromOnNewIntent: Boolean = false
|
private val isFromOnNewIntent: Boolean = false
|
||||||
|
|
@ -112,7 +110,6 @@ class DeepLinkFactoryTest {
|
||||||
promoDeepLink = promoDeepLinkFactory,
|
promoDeepLink = promoDeepLinkFactory,
|
||||||
onboardVisaDeepLink = onboardVisaDeepLink,
|
onboardVisaDeepLink = onboardVisaDeepLink,
|
||||||
newsDetailsDeepLink = newsDeeplink,
|
newsDetailsDeepLink = newsDeeplink,
|
||||||
feedFeatureToggle = feedFeatureToggle,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(deps.plugins.kotlin.android) apply false
|
alias(deps.plugins.kotlin.android) apply false
|
||||||
alias(deps.plugins.kotlin.jvm) apply false
|
alias(deps.plugins.kotlin.jvm) apply false
|
||||||
|
|
@ -33,83 +30,13 @@ interface Injected {
|
||||||
val fs: FileSystemOperations
|
val fs: FileSystemOperations
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestStats(
|
|
||||||
val total: Long = 0,
|
|
||||||
val passed: Long = 0,
|
|
||||||
val failed: Long = 0,
|
|
||||||
val skipped: Long = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
val testResultsByModule = ConcurrentHashMap<String, TestStats>()
|
|
||||||
|
|
||||||
// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules
|
// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules
|
||||||
val unitTest by tasks.registering {
|
val unitTest by tasks.registering {
|
||||||
group = "verification"
|
group = "verification"
|
||||||
description = "Run unit tests for debug/googleDebug variant and all JVM modules"
|
description = "Run unit tests for debug/googleDebug variant and all JVM modules"
|
||||||
|
|
||||||
doLast {
|
|
||||||
if (testResultsByModule.isNotEmpty()) {
|
|
||||||
val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats ->
|
|
||||||
TestStats(
|
|
||||||
total = acc.total + stats.total,
|
|
||||||
passed = acc.passed + stats.passed,
|
|
||||||
failed = acc.failed + stats.failed,
|
|
||||||
skipped = acc.skipped + stats.skipped,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
println("\n" + "=".repeat(80))
|
|
||||||
println("TEST SUMMARY")
|
|
||||||
println("=".repeat(80))
|
|
||||||
|
|
||||||
testResultsByModule.toSortedMap().forEach { (module, stats) ->
|
|
||||||
println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)")
|
|
||||||
}
|
|
||||||
|
|
||||||
println("-".repeat(80))
|
|
||||||
println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules")
|
|
||||||
println(" Passed: ${totalStats.passed}")
|
|
||||||
println(" Failed: ${totalStats.failed}")
|
|
||||||
println(" Skipped: ${totalStats.skipped}")
|
|
||||||
println("=".repeat(80))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test Logging and testCI dependencies
|
|
||||||
subprojects {
|
subprojects {
|
||||||
tasks.withType<Test>().configureEach {
|
|
||||||
println("Test task scheduled: $path")
|
|
||||||
|
|
||||||
testLogging {
|
|
||||||
exceptionFormat = TestExceptionFormat.FULL
|
|
||||||
showStandardStreams = true
|
|
||||||
|
|
||||||
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
|
|
||||||
if (desc.parent == null) { // will match the outermost suite
|
|
||||||
testResultsByModule[path] = TestStats(
|
|
||||||
total = result.testCount,
|
|
||||||
passed = result.successfulTestCount,
|
|
||||||
failed = result.failedTestCount,
|
|
||||||
skipped = result.skippedTestCount,
|
|
||||||
)
|
|
||||||
|
|
||||||
val output =
|
|
||||||
"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
|
|
||||||
val startItem = "| "
|
|
||||||
val endItem = " |"
|
|
||||||
val repeatLength = startItem.length + output.length + endItem.length
|
|
||||||
println(
|
|
||||||
"\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(
|
|
||||||
repeatLength
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register testCI dependencies
|
|
||||||
// App module
|
// App module
|
||||||
plugins.withId("com.android.application") {
|
plugins.withId("com.android.application") {
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
|
|
|
||||||
|
|
@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EditAccount(
|
data class EditAccount(
|
||||||
val account: Account,
|
val account: Account.CryptoPortfolio,
|
||||||
) : AppRoute(path = "/edit_account/${account.accountId.value}")
|
) : AppRoute(path = "/edit_account/${account.accountId.value}")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AccountDetails(
|
data class AccountDetails(
|
||||||
val account: Account,
|
val account: Account.CryptoPortfolio,
|
||||||
) : AppRoute(path = "/account_details/${account.accountId.value}")
|
) : AppRoute(path = "/account_details/${account.accountId.value}")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
object TangemSiteUrlBuilder {
|
object TangemSiteUrlBuilder {
|
||||||
|
|
||||||
|
const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10"
|
||||||
|
|
||||||
suspend fun getUtmTags(campaign: String?): String {
|
suspend fun getUtmTags(campaign: String?): String {
|
||||||
val langCode = Locale.getDefault().language
|
val langCode = Locale.getDefault().language
|
||||||
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()
|
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ object MockScanResponseFactory {
|
||||||
CardDTO.Wallet(
|
CardDTO.Wallet(
|
||||||
CardWallet(
|
CardWallet(
|
||||||
publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name
|
publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name
|
||||||
chainCode = null,
|
chainCode = ByteArray(32), // chainCode must not be null for HD wallets
|
||||||
curve = curve,
|
curve = curve,
|
||||||
settings = createSettings(),
|
settings = createSettings(),
|
||||||
totalSignedHashes = null,
|
totalSignedHashes = null,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter(
|
||||||
)
|
)
|
||||||
return TokenItemState.Content(
|
return TokenItemState.Content(
|
||||||
id = account.accountId.toItemId(),
|
id = account.accountId.toItemId(),
|
||||||
iconState = AccountIconItemStateConverter.convert(this),
|
iconState = AccountIconItemStateConverter().convert(this),
|
||||||
titleState = TokenItemState.TitleState.Content(
|
titleState = TokenItemState.TitleState.Content(
|
||||||
text = accountName.toUM().value,
|
text = accountName.toUM().value,
|
||||||
),
|
),
|
||||||
|
|
@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter(
|
||||||
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
|
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
|
||||||
return TokenItemState.Content(
|
return TokenItemState.Content(
|
||||||
id = account.accountId.toItemId(),
|
id = account.accountId.toItemId(),
|
||||||
iconState = AccountIconItemStateConverter.convert(account),
|
iconState = AccountIconItemStateConverter().convert(account),
|
||||||
titleState = TokenItemState.TitleState.Content(
|
titleState = TokenItemState.TitleState.Content(
|
||||||
text = accountName.toUM().value,
|
text = accountName.toUM().value,
|
||||||
),
|
),
|
||||||
|
|
@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter(
|
||||||
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
|
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
|
||||||
return TokenItemState.Unreachable(
|
return TokenItemState.Unreachable(
|
||||||
id = account.accountId.toItemId(),
|
id = account.accountId.toItemId(),
|
||||||
iconState = AccountIconItemStateConverter.convert(account),
|
iconState = AccountIconItemStateConverter().convert(account),
|
||||||
titleState = TokenItemState.TitleState.Content(
|
titleState = TokenItemState.TitleState.Content(
|
||||||
text = accountName.toUM().value,
|
text = accountName.toUM().value,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package com.tangem.common.ui.account
|
package com.tangem.common.ui.account
|
||||||
|
|
||||||
|
import com.tangem.core.ui.components.account.AccountIconSize
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
||||||
object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.CryptoPortfolio> {
|
class AccountIconItemStateConverter(
|
||||||
|
val size: AccountIconSize = AccountIconSize.Default,
|
||||||
|
) : Converter<Account, CurrencyIconState.CryptoPortfolio> {
|
||||||
|
|
||||||
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
|
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
|
||||||
is Account.CryptoPortfolio -> when {
|
is Account.CryptoPortfolio -> when {
|
||||||
|
|
@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
|
||||||
char = value.accountName.toUM().value,
|
char = value.accountName.toUM().value,
|
||||||
color = value.icon.color.getUiColor(),
|
color = value.icon.color.getUiColor(),
|
||||||
isGrayscale = false,
|
isGrayscale = false,
|
||||||
|
size = size,
|
||||||
)
|
)
|
||||||
else -> CurrencyIconState.CryptoPortfolio.Icon(
|
else -> CurrencyIconState.CryptoPortfolio.Icon(
|
||||||
resId = value.icon.value.getResId(),
|
resId = value.icon.value.getResId(),
|
||||||
color = value.icon.color.getUiColor(),
|
color = value.icon.color.getUiColor(),
|
||||||
isGrayscale = false,
|
isGrayscale = false,
|
||||||
|
size = size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
package com.tangem.common.ui.alerts
|
|
||||||
|
|
||||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
|
||||||
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
|
|
||||||
import com.tangem.common.ui.alerts.models.AlertUM
|
|
||||||
import com.tangem.core.ui.R
|
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
|
||||||
import com.tangem.domain.transaction.error.SendTransactionError
|
|
||||||
import com.tangem.utils.converter.Converter
|
|
||||||
|
|
||||||
class TransactionErrorAlertConverter(
|
|
||||||
private val popBackStack: () -> Unit,
|
|
||||||
private val onFailedTxEmailClick: (String) -> Unit,
|
|
||||||
) : Converter<SendTransactionError, AlertUM?> {
|
|
||||||
override fun convert(value: SendTransactionError): AlertUM? {
|
|
||||||
return when (value) {
|
|
||||||
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
|
|
||||||
onConfirmClick = popBackStack,
|
|
||||||
)
|
|
||||||
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
|
|
||||||
code = value.code.toString(),
|
|
||||||
cause = null,
|
|
||||||
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
|
|
||||||
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
|
|
||||||
)
|
|
||||||
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
|
|
||||||
code = value.code.toString(),
|
|
||||||
cause = value.message,
|
|
||||||
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
|
|
||||||
)
|
|
||||||
is SendTransactionError.DataError -> AlertTransactionErrorUM(
|
|
||||||
code = "",
|
|
||||||
cause = value.message,
|
|
||||||
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
|
|
||||||
)
|
|
||||||
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
|
|
||||||
code = value.code.orEmpty(),
|
|
||||||
cause = value.message.orEmpty(),
|
|
||||||
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
|
|
||||||
)
|
|
||||||
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
|
|
||||||
code = "",
|
|
||||||
cause = value.ex?.localizedMessage,
|
|
||||||
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
|
||||||
)
|
|
||||||
is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM(
|
|
||||||
code = "",
|
|
||||||
cause = null,
|
|
||||||
causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)),
|
|
||||||
onConfirmClick = popBackStack,
|
|
||||||
)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package com.tangem.common.ui.alerts
|
||||||
|
|
||||||
|
import com.tangem.common.ui.R
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
|
import com.tangem.core.ui.message.EventMessageAction
|
||||||
|
import com.tangem.domain.transaction.error.SendTransactionError
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class TransactionErrorDialogFactory @Inject constructor() {
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
error: SendTransactionError,
|
||||||
|
popBackStack: () -> Unit,
|
||||||
|
onFailedTxEmailClick: (String) -> Unit,
|
||||||
|
): DialogMessage? {
|
||||||
|
return when (error) {
|
||||||
|
is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack)
|
||||||
|
is SendTransactionError.TangemSdkError -> transactionErrorDialog(
|
||||||
|
causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)),
|
||||||
|
code = error.code.toString(),
|
||||||
|
onConfirmClick = { onFailedTxEmailClick(error.code.toString()) },
|
||||||
|
)
|
||||||
|
is SendTransactionError.BlockchainSdkError -> transactionErrorDialog(
|
||||||
|
cause = error.message,
|
||||||
|
code = error.code.toString(),
|
||||||
|
onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") },
|
||||||
|
)
|
||||||
|
is SendTransactionError.DataError -> transactionErrorDialog(
|
||||||
|
cause = error.message,
|
||||||
|
code = "",
|
||||||
|
onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
|
||||||
|
)
|
||||||
|
is SendTransactionError.NetworkError -> transactionErrorDialog(
|
||||||
|
cause = error.message.orEmpty(),
|
||||||
|
code = error.code.orEmpty(),
|
||||||
|
onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
|
||||||
|
)
|
||||||
|
is SendTransactionError.UnknownError -> transactionErrorDialog(
|
||||||
|
cause = error.ex?.localizedMessage,
|
||||||
|
code = "",
|
||||||
|
onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) },
|
||||||
|
)
|
||||||
|
is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog(
|
||||||
|
causeTextReference = resourceReference(
|
||||||
|
R.string.no_account_polkadot,
|
||||||
|
wrappedList(error.amount),
|
||||||
|
),
|
||||||
|
code = "",
|
||||||
|
onConfirmClick = popBackStack,
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage(
|
||||||
|
title = resourceReference(id = R.string.warning_demo_mode_title),
|
||||||
|
message = resourceReference(id = R.string.warning_demo_mode_message),
|
||||||
|
firstAction = EventMessageAction(
|
||||||
|
title = resourceReference(id = R.string.common_ok),
|
||||||
|
onClick = onConfirmClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun transactionErrorDialog(
|
||||||
|
cause: String? = null,
|
||||||
|
causeTextReference: TextReference? = null,
|
||||||
|
code: String,
|
||||||
|
onConfirmClick: () -> Unit,
|
||||||
|
): DialogMessage = DialogMessage(
|
||||||
|
title = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||||
|
message = resourceReference(
|
||||||
|
id = R.string.send_alert_transaction_failed_text,
|
||||||
|
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
||||||
|
),
|
||||||
|
firstAction = EventMessageAction(
|
||||||
|
title = resourceReference(id = R.string.common_support),
|
||||||
|
onClick = onConfirmClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package com.tangem.common.ui.alerts.models
|
|
||||||
|
|
||||||
import com.tangem.common.ui.R
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
|
||||||
|
|
||||||
data class AlertDemoModeUM(
|
|
||||||
override val onConfirmClick: () -> Unit,
|
|
||||||
) : AlertUM {
|
|
||||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
|
||||||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
|
||||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package com.tangem.common.ui.alerts.models
|
|
||||||
|
|
||||||
import com.tangem.common.ui.R
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
|
||||||
|
|
||||||
data class AlertTransactionErrorUM(
|
|
||||||
val code: String,
|
|
||||||
val cause: String?,
|
|
||||||
val causeTextReference: TextReference? = null,
|
|
||||||
override val onConfirmClick: () -> Unit,
|
|
||||||
) : AlertUM {
|
|
||||||
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
|
|
||||||
override val message: TextReference = resourceReference(
|
|
||||||
id = R.string.send_alert_transaction_failed_text,
|
|
||||||
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
|
||||||
)
|
|
||||||
override val confirmButtonText: TextReference =
|
|
||||||
resourceReference(id = R.string.common_support)
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package com.tangem.common.ui.alerts.models
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
interface AlertUM {
|
|
||||||
val title: TextReference?
|
|
||||||
val message: TextReference
|
|
||||||
val confirmButtonText: TextReference
|
|
||||||
val onConfirmClick: (() -> Unit)?
|
|
||||||
}
|
|
||||||
|
|
@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Deprecated("Use GiveApprovalComponent")
|
||||||
@Composable
|
@Composable
|
||||||
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
|
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
|
||||||
var isPermissionAlertShow by remember { mutableStateOf(false) }
|
var isPermissionAlertShow by remember { mutableStateOf(false) }
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,35 @@
|
||||||
package com.tangem.common.ui.notifications
|
package com.tangem.common.ui.notifications
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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 androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.components.notifications.Notification
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
|
import com.tangem.core.ui.ds.TangemPagerIndicator
|
||||||
import com.tangem.core.ui.ds.message.TangemMessage
|
import com.tangem.core.ui.ds.message.TangemMessage
|
||||||
|
import com.tangem.core.ui.ds.message.TangemMessageEffect
|
||||||
import com.tangem.core.ui.ds.message.TangemMessageUM
|
import com.tangem.core.ui.ds.message.TangemMessageUM
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
fun LazyListScope.notifications(
|
fun LazyListScope.notifications(
|
||||||
notifications: ImmutableList<NotificationUM>,
|
notifications: ImmutableList<NotificationUM>,
|
||||||
|
|
@ -106,8 +124,107 @@ fun LazyListScope.notifications(
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(top = topPadding)
|
.padding(top = topPadding)
|
||||||
.animateItem(),
|
.animateItem(null, null, null),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a list of notifications in a stacked manner using a HorizontalPager.
|
||||||
|
* If there are multiple notifications, a PagerIndicator is shown below the notifications.
|
||||||
|
*
|
||||||
|
* @param notifications List of TangemMessageUM objects to be displayed.
|
||||||
|
* @param containerColor Color to be used for the background of the notifications.
|
||||||
|
* @param modifier Optional Modifier for the notifications.
|
||||||
|
*/
|
||||||
|
fun LazyListScope.stackedNotifications(
|
||||||
|
notifications: ImmutableList<TangemMessageUM>?,
|
||||||
|
containerColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (!notifications.isNullOrEmpty()) {
|
||||||
|
val notificationsPagerState = rememberPagerState(
|
||||||
|
pageCount = { notifications.size },
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = TangemTheme.dimens2.x2),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = notificationsPagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.animateItem(null, null, null),
|
||||||
|
) { page ->
|
||||||
|
TangemMessage(
|
||||||
|
messageUM = notifications[page],
|
||||||
|
contentColor = containerColor,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (notifications.size > 1) {
|
||||||
|
TangemPagerIndicator(
|
||||||
|
pagerState = notificationsPagerState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Preview
|
||||||
|
@Composable
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
private fun StackedNotifications_Preview(
|
||||||
|
@PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList<TangemMessageUM>,
|
||||||
|
) {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
val contentColor = TangemTheme.colors2.surface.level1
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(contentColor)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
stackedNotifications(
|
||||||
|
notifications = params,
|
||||||
|
containerColor = contentColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StackedNotificationsPreviewProvider : PreviewParameterProvider<ImmutableList<TangemMessageUM>> {
|
||||||
|
override val values: Sequence<ImmutableList<TangemMessageUM>>
|
||||||
|
get() = sequenceOf(
|
||||||
|
persistentListOf(
|
||||||
|
TangemMessageUM(
|
||||||
|
id = "0",
|
||||||
|
title = stringReference("First notification"),
|
||||||
|
subtitle = stringReference("This is the first notification"),
|
||||||
|
messageEffect = TangemMessageEffect.Magic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
persistentListOf(
|
||||||
|
TangemMessageUM(
|
||||||
|
id = "0",
|
||||||
|
title = stringReference("First notification"),
|
||||||
|
subtitle = stringReference("This is the first notification"),
|
||||||
|
messageEffect = TangemMessageEffect.Magic,
|
||||||
|
),
|
||||||
|
TangemMessageUM(
|
||||||
|
id = "1",
|
||||||
|
title = stringReference("Second notification"),
|
||||||
|
subtitle = stringReference("This is the second notification"),
|
||||||
|
messageEffect = TangemMessageEffect.Card,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig
|
||||||
import com.tangem.core.abtests.manager.ABTestsManager
|
import com.tangem.core.abtests.manager.ABTestsManager
|
||||||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.utils.Provider
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -24,7 +23,7 @@ internal object ABTestsManagerModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideABTestsManager(
|
fun provideABTestsManager(
|
||||||
application: Application,
|
application: Application,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfig: EnvironmentConfig,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): ABTestsManager {
|
): ABTestsManager {
|
||||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||||
|
|
@ -32,7 +31,7 @@ internal object ABTestsManagerModule {
|
||||||
} else {
|
} else {
|
||||||
AmplitudeABTestsManager(
|
AmplitudeABTestsManager(
|
||||||
application = application,
|
application = application,
|
||||||
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
|
apiKey = environmentConfig.amplitudeApiKey,
|
||||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@ import com.amplitude.experiment.ExperimentConfig
|
||||||
import com.amplitude.experiment.ExperimentUser
|
import com.amplitude.experiment.ExperimentUser
|
||||||
import com.tangem.core.abtests.manager.ABTestsManager
|
import com.tangem.core.abtests.manager.ABTestsManager
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.utils.Provider
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
internal class AmplitudeABTestsManager(
|
internal class AmplitudeABTestsManager(
|
||||||
val application: Application,
|
val application: Application,
|
||||||
val apiKeyProvider: Provider<String>,
|
val apiKey: String,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
) : ABTestsManager {
|
) : ABTestsManager {
|
||||||
|
|
||||||
|
|
@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager(
|
||||||
|
|
||||||
client = Experiment.initializeWithAmplitudeAnalytics(
|
client = Experiment.initializeWithAmplitudeAnalytics(
|
||||||
application = application,
|
application = application,
|
||||||
apiKey = apiKeyProvider(),
|
apiKey = apiKey,
|
||||||
config = ExperimentConfig
|
config = ExperimentConfig
|
||||||
.builder()
|
.builder()
|
||||||
.automaticFetchOnAmplitudeIdentityChange(true)
|
.automaticFetchOnAmplitudeIdentityChange(true)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.core.analytics.models.event
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offramp (withdraw/sell) analytics events
|
||||||
|
*/
|
||||||
|
sealed class OfframpAnalyticsEvent(
|
||||||
|
event: String,
|
||||||
|
params: Map<String, String> = emptyMap(),
|
||||||
|
) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw screen opened event
|
||||||
|
*/
|
||||||
|
data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened")
|
||||||
|
}
|
||||||
|
|
@ -7,14 +7,7 @@
|
||||||
"name": "VISA_ONBOARDING_ENABLED",
|
"name": "VISA_ONBOARDING_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "STAKING_TON_ENABLED",
|
|
||||||
"version": "5.28.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "STAKING_CARDANO_ENABLED",
|
|
||||||
"version": "5.31.1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "STAKING_ETH_ENABLED",
|
"name": "STAKING_ETH_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
@ -31,30 +24,10 @@
|
||||||
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
||||||
"version": "5.32.0"
|
"version": "5.32.0"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "TANGEM_PAY_ENABLED",
|
|
||||||
"version": "5.31.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
|
|
||||||
"version": "5.30.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
|
|
||||||
"version": "5.33.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
|
||||||
"version": "5.31.0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||||
"version": "5.33.0"
|
"version": "5.33.0"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "FEED_ENABLED",
|
|
||||||
"version": "5.33.0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "APP_REDESIGN_ENABLED",
|
"name": "APP_REDESIGN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
@ -78,5 +51,17 @@
|
||||||
{
|
{
|
||||||
"name": "WALLET_REORDER_FEATURE_ENABLED",
|
"name": "WALLET_REORDER_FEATURE_ENABLED",
|
||||||
"version": "5.34"
|
"version": "5.34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GASLESS_APPROVAL_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MULTI_ADDRESS_UTXO_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator
|
||||||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||||
|
import com.tangem.plugin.configuration.model.BuildType
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(deps.plugins.android.library)
|
alias(deps.plugins.android.library)
|
||||||
|
|
@ -10,14 +12,53 @@ plugins {
|
||||||
id("configuration")
|
id("configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class GenerateEnvironmentConfigTask : DefaultTask() {
|
||||||
|
|
||||||
|
@get:InputFile
|
||||||
|
abstract val configFile: RegularFileProperty
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun generate() {
|
||||||
|
val input = configFile.get().asFile
|
||||||
|
require(input.exists()) { "Config file not found: ${input.absolutePath}" }
|
||||||
|
logger.lifecycle("Generating EnvironmentConfig from ${input.name}")
|
||||||
|
EnvironmentConfigGenerator.generate(input, outputDir.get().asFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.tangem.datasource"
|
namespace = "com.tangem.datasource"
|
||||||
|
|
||||||
|
sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config"))
|
||||||
|
|
||||||
room {
|
room {
|
||||||
schemaDirectory("$projectDir/schemas")
|
schemaDirectory("$projectDir/schemas")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug
|
||||||
|
val configFile = rootProject.file(
|
||||||
|
"app/src/main/assets/tangem-app-config/config_${buildType.environment}.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks.register<GenerateEnvironmentConfigTask>(
|
||||||
|
"generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}",
|
||||||
|
) {
|
||||||
|
this.configFile.set(configFile)
|
||||||
|
outputDir.set(layout.buildDirectory.dir("generated/source/environment-config"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("preBuild") {
|
||||||
|
dependsOn(tasks.matching { it.name.startsWith("generateEnvironmentConfig") })
|
||||||
|
}
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
tasks.withType<Test>().configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package com.tangem.datasource.api.common.config
|
package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
internal class BlockAid(
|
internal class BlockAid(
|
||||||
private val configStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
) : ApiConfig() {
|
) : ApiConfig() {
|
||||||
|
|
||||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||||
|
|
@ -21,9 +20,7 @@ internal class BlockAid(
|
||||||
put(
|
put(
|
||||||
key = "X-API-KEY",
|
key = "X-API-KEY",
|
||||||
value = ProviderSuspend {
|
value = ProviderSuspend {
|
||||||
requireNotNull(
|
requireNotNull(environmentConfig.blockAidApiKey)
|
||||||
configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
put("accept", ProviderSuspend { "application/json" })
|
put("accept", ProviderSuspend { "application/json" })
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.datasource.api.common.config
|
package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.utils.RequestHeader
|
import com.tangem.datasource.utils.RequestHeader
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
|
|
@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider
|
||||||
/**
|
/**
|
||||||
* Express [ApiConfig]
|
* Express [ApiConfig]
|
||||||
*
|
*
|
||||||
* @property environmentConfigStorage environment config storage
|
* @property environmentConfig environment config
|
||||||
* @property expressAuthProvider express auth provider
|
* @property expressAuthProvider express auth provider
|
||||||
* @property appVersionProvider app version provider
|
* @property appVersionProvider app version provider
|
||||||
* @property appInfoProvider app info provider
|
* @property appInfoProvider app info provider
|
||||||
*/
|
*/
|
||||||
internal class Express(
|
internal class Express(
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
private val expressAuthProvider: ExpressAuthProvider,
|
private val expressAuthProvider: ExpressAuthProvider,
|
||||||
private val appVersionProvider: AppVersionProvider,
|
private val appVersionProvider: AppVersionProvider,
|
||||||
private val appInfoProvider: AppInfoProvider,
|
private val appInfoProvider: AppInfoProvider,
|
||||||
|
|
@ -100,9 +100,9 @@ internal class Express(
|
||||||
|
|
||||||
private fun getApiKey(isProd: Boolean): String {
|
private fun getApiKey(isProd: Boolean): String {
|
||||||
return if (isProd) {
|
return if (isProd) {
|
||||||
environmentConfigStorage.getConfigSync().express
|
environmentConfig.express
|
||||||
} else {
|
} else {
|
||||||
environmentConfigStorage.getConfigSync().devExpress
|
environmentConfig.devExpress
|
||||||
}
|
}
|
||||||
?.apiKey
|
?.apiKey
|
||||||
?: error("No express config provided")
|
?: error("No express config provided")
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.tangem.datasource.api.common.config
|
package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
import com.tangem.utils.version.AppVersionProvider
|
import com.tangem.utils.version.AppVersionProvider
|
||||||
|
|
||||||
internal sealed class TangemPay(
|
internal sealed class TangemPay(
|
||||||
|
private val environmentConfig: EnvironmentConfig,
|
||||||
private val appVersionProvider: AppVersionProvider,
|
private val appVersionProvider: AppVersionProvider,
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
|
||||||
) : ApiConfig() {
|
) : ApiConfig() {
|
||||||
|
|
||||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||||
|
|
@ -61,8 +61,8 @@ internal sealed class TangemPay(
|
||||||
return when (apiEnvironment) {
|
return when (apiEnvironment) {
|
||||||
ApiEnvironment.MOCK,
|
ApiEnvironment.MOCK,
|
||||||
ApiEnvironment.DEV,
|
ApiEnvironment.DEV,
|
||||||
-> environmentConfigStorage.getConfigSync().bffStaticTokenDev
|
-> environmentConfig.bffStaticTokenDev
|
||||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken
|
ApiEnvironment.PROD -> environmentConfig.bffStaticToken
|
||||||
ApiEnvironment.STAGE,
|
ApiEnvironment.STAGE,
|
||||||
ApiEnvironment.STAGE_2,
|
ApiEnvironment.STAGE_2,
|
||||||
ApiEnvironment.DEV_2,
|
ApiEnvironment.DEV_2,
|
||||||
|
|
@ -72,9 +72,9 @@ internal sealed class TangemPay(
|
||||||
}
|
}
|
||||||
|
|
||||||
class Bff(
|
class Bff(
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
) : TangemPay(environmentConfig, appVersionProvider) {
|
||||||
) : TangemPay(appVersionProvider, environmentConfigStorage) {
|
|
||||||
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
||||||
return when (apiEnvironment) {
|
return when (apiEnvironment) {
|
||||||
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
|
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
|
||||||
|
|
@ -90,9 +90,9 @@ internal sealed class TangemPay(
|
||||||
}
|
}
|
||||||
|
|
||||||
class Auth(
|
class Auth(
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
) : TangemPay(environmentConfig, appVersionProvider) {
|
||||||
) : TangemPay(appVersionProvider, environmentConfigStorage) {
|
|
||||||
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
||||||
return when (apiEnvironment) {
|
return when (apiEnvironment) {
|
||||||
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"
|
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.utils.RequestHeader
|
import com.tangem.datasource.utils.RequestHeader
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
import com.tangem.utils.info.AppInfoProvider
|
import com.tangem.utils.info.AppInfoProvider
|
||||||
|
|
@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider
|
||||||
|
|
||||||
/** YieldSupply [ApiConfig] */
|
/** YieldSupply [ApiConfig] */
|
||||||
internal class YieldSupply(
|
internal class YieldSupply(
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
private val appVersionProvider: AppVersionProvider,
|
private val appVersionProvider: AppVersionProvider,
|
||||||
private val authProvider: AuthProvider,
|
private val authProvider: AuthProvider,
|
||||||
private val appInfoProvider: AppInfoProvider,
|
private val appInfoProvider: AppInfoProvider,
|
||||||
|
|
@ -78,8 +78,8 @@ internal class YieldSupply(
|
||||||
ApiEnvironment.DEV_3,
|
ApiEnvironment.DEV_3,
|
||||||
ApiEnvironment.STAGE,
|
ApiEnvironment.STAGE,
|
||||||
ApiEnvironment.STAGE_2,
|
ApiEnvironment.STAGE_2,
|
||||||
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
|
-> environmentConfig.yieldModuleApiKeyDev
|
||||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
|
ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey
|
||||||
} ?: error("No tangem tech api config provided")
|
} ?: error("No tangem tech api config provided")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ data class OrderResponse(
|
||||||
@Json(name = "id") val id: String,
|
@Json(name = "id") val id: String,
|
||||||
@Json(name = "customer_id") val customerId: String?,
|
@Json(name = "customer_id") val customerId: String?,
|
||||||
@Json(name = "type") val type: String?,
|
@Json(name = "type") val type: String?,
|
||||||
@Json(name = "status") val status: String,
|
@Json(name = "status") val status: Status,
|
||||||
@Json(name = "step") val step: String?,
|
@Json(name = "step") val step: String?,
|
||||||
@Json(name = "data") val data: Data,
|
@Json(name = "data") val data: Data,
|
||||||
@Json(name = "step_change_code") val stepChangeCode: Int?,
|
@Json(name = "step_change_code") val stepChangeCode: Int?,
|
||||||
|
|
@ -29,5 +29,20 @@ data class OrderResponse(
|
||||||
@Json(name = "payment_account_id") val paymentAccountId: String?,
|
@Json(name = "payment_account_id") val paymentAccountId: String?,
|
||||||
@Json(name = "transaction_hash") val transactionHash: String?,
|
@Json(name = "transaction_hash") val transactionHash: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
|
enum class Status {
|
||||||
|
@Json(name = "NEW")
|
||||||
|
NEW,
|
||||||
|
|
||||||
|
@Json(name = "PROCESSING")
|
||||||
|
PROCESSING,
|
||||||
|
|
||||||
|
@Json(name = "COMPLETED")
|
||||||
|
COMPLETED,
|
||||||
|
|
||||||
|
@Json(name = "CANCELED")
|
||||||
|
CANCELED,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig
|
import com.tangem.datasource.api.common.config.ApiConfig
|
||||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
|
|
||||||
internal class Sha256SignatureVerifier(
|
internal class Sha256SignatureVerifier(
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfig: EnvironmentConfig,
|
||||||
private val apiConfigsManager: ApiConfigsManager,
|
private val apiConfigsManager: ApiConfigsManager,
|
||||||
) : DataSignatureVerifier {
|
) : DataSignatureVerifier {
|
||||||
|
|
||||||
|
|
@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier(
|
||||||
private fun getPubKey(): String? {
|
private fun getPubKey(): String? {
|
||||||
val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express)
|
val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express)
|
||||||
return when (expressConfig.environment) {
|
return when (expressConfig.environment) {
|
||||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey
|
ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey
|
||||||
else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey
|
else -> environmentConfig.devExpress?.signVerifierPublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ package com.tangem.datasource.di
|
||||||
|
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.api.common.config.*
|
import com.tangem.datasource.api.common.config.*
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||||
|
|
@ -21,13 +21,13 @@ internal object ApiConfigsModule {
|
||||||
@Provides
|
@Provides
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideExpressConfig(
|
fun provideExpressConfig(
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfig: EnvironmentConfig,
|
||||||
expressAuthProvider: ExpressAuthProvider,
|
expressAuthProvider: ExpressAuthProvider,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
appInfoProvider: AppInfoProvider,
|
appInfoProvider: AppInfoProvider,
|
||||||
): ApiConfig {
|
): ApiConfig {
|
||||||
return Express(
|
return Express(
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfig = environmentConfig,
|
||||||
expressAuthProvider = expressAuthProvider,
|
expressAuthProvider = expressAuthProvider,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
appInfoProvider = appInfoProvider,
|
appInfoProvider = appInfoProvider,
|
||||||
|
|
@ -73,12 +73,12 @@ internal object ApiConfigsModule {
|
||||||
@Provides
|
@Provides
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideYieldSupplyConfig(
|
fun provideYieldSupplyConfig(
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfig: EnvironmentConfig,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
authProvider: AuthProvider,
|
authProvider: AuthProvider,
|
||||||
appInfoProvider: AppInfoProvider,
|
appInfoProvider: AppInfoProvider,
|
||||||
): ApiConfig = YieldSupply(
|
): ApiConfig = YieldSupply(
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
authProvider = authProvider,
|
authProvider = authProvider,
|
||||||
appInfoProvider = appInfoProvider,
|
appInfoProvider = appInfoProvider,
|
||||||
|
|
@ -87,21 +87,21 @@ internal object ApiConfigsModule {
|
||||||
@Provides
|
@Provides
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideTangemPayBffConfig(
|
fun provideTangemPayBffConfig(
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider)
|
||||||
): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideTangemPayAuthConfig(
|
fun provideTangemPayAuthConfig(
|
||||||
|
environmentConfig: EnvironmentConfig,
|
||||||
appVersionProvider: AppVersionProvider,
|
appVersionProvider: AppVersionProvider,
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider)
|
||||||
): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
|
fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig {
|
||||||
return BlockAid(environmentConfigStorage)
|
return BlockAid(environmentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
|
||||||
import com.tangem.datasource.api.common.adapter.*
|
import com.tangem.datasource.api.common.adapter.*
|
||||||
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
||||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||||
|
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
|
||||||
import com.tangem.datasource.utils.SerializeNullsFactory
|
import com.tangem.datasource.utils.SerializeNullsFactory
|
||||||
import com.tangem.domain.models.scan.serialization.*
|
import com.tangem.domain.models.scan.serialization.*
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -45,6 +46,15 @@ class MoshiModule {
|
||||||
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
|
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
|
||||||
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
|
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
|
||||||
)
|
)
|
||||||
|
.add(
|
||||||
|
NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java)
|
||||||
|
.withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created")
|
||||||
|
.withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status")
|
||||||
|
.withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card")
|
||||||
|
.withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked")
|
||||||
|
.withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance")
|
||||||
|
.withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"),
|
||||||
|
)
|
||||||
.add(
|
.add(
|
||||||
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
||||||
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
|
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.datasource.di
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||||
import com.tangem.datasource.crypto.Sha256SignatureVerifier
|
import com.tangem.datasource.crypto.Sha256SignatureVerifier
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -17,9 +17,9 @@ internal object SecurityModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDataSignatureVerifier(
|
fun provideDataSignatureVerifier(
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfig: EnvironmentConfig,
|
||||||
apiConfigsManager: ApiConfigsManager,
|
apiConfigsManager: ApiConfigsManager,
|
||||||
): DataSignatureVerifier {
|
): DataSignatureVerifier {
|
||||||
return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager)
|
return Sha256SignatureVerifier(environmentConfig, apiConfigsManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
package com.tangem.datasource.di.local.config
|
package com.tangem.datasource.di.local.config
|
||||||
|
|
||||||
import com.tangem.datasource.asset.loader.AssetLoader
|
import com.tangem.datasource.asset.loader.AssetLoader
|
||||||
import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter
|
||||||
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
|
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
|
||||||
|
|
@ -23,11 +22,8 @@ internal object ConfigModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage {
|
fun provideEnvironmentConfig(): EnvironmentConfig {
|
||||||
return DefaultEnvironmentConfigStorage(
|
return GeneratedEnvironmentConfigConverter.convert()
|
||||||
assetLoader = assetLoader,
|
|
||||||
environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package com.tangem.datasource.local.config.environment
|
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
|
||||||
import com.tangem.datasource.asset.loader.AssetLoader
|
|
||||||
import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter
|
|
||||||
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
|
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation for storing [EnvironmentConfig]
|
|
||||||
*
|
|
||||||
* @property assetLoader asset loader
|
|
||||||
* @property environmentConfigStore config store
|
|
||||||
*/
|
|
||||||
internal class DefaultEnvironmentConfigStorage(
|
|
||||||
private val assetLoader: AssetLoader,
|
|
||||||
private val environmentConfigStore: RuntimeStateStore<EnvironmentConfig>,
|
|
||||||
) : EnvironmentConfigStorage {
|
|
||||||
|
|
||||||
override suspend fun initialize(): EnvironmentConfig {
|
|
||||||
val environmentConfigModel = assetLoader.load<EnvironmentConfigModel>(fileName = CONFIG_FILE_NAME)
|
|
||||||
?: return environmentConfigStore.get().value
|
|
||||||
|
|
||||||
val config = EnvironmentConfigConverter.convert(value = environmentConfigModel)
|
|
||||||
environmentConfigStore.store(value = config)
|
|
||||||
|
|
||||||
Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully")
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getConfig(): Flow<EnvironmentConfig> = environmentConfigStore.get()
|
|
||||||
|
|
||||||
override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package com.tangem.datasource.local.config.environment
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage for [EnvironmentConfig]
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
interface EnvironmentConfigStorage {
|
|
||||||
|
|
||||||
/** Initialize and return [EnvironmentConfig] */
|
|
||||||
suspend fun initialize(): EnvironmentConfig
|
|
||||||
|
|
||||||
/** Get [EnvironmentConfig] as [Flow] */
|
|
||||||
fun getConfig(): Flow<EnvironmentConfig>
|
|
||||||
|
|
||||||
/** Get [EnvironmentConfig] synchronously */
|
|
||||||
fun getConfigSync(): EnvironmentConfig
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
package com.tangem.datasource.local.config.environment.converter
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.*
|
||||||
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey
|
||||||
|
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey
|
||||||
|
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||||
|
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig]
|
||||||
|
*
|
||||||
|
* This converter maps the auto-generated config (from JSON) to the domain model.
|
||||||
|
* The generated config has nested objects that mirror the JSON structure.
|
||||||
|
*/
|
||||||
|
internal object GeneratedEnvironmentConfigConverter {
|
||||||
|
|
||||||
|
fun convert(): EnvironmentConfig {
|
||||||
|
return EnvironmentConfig(
|
||||||
|
moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey,
|
||||||
|
moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey,
|
||||||
|
mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId,
|
||||||
|
mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret,
|
||||||
|
blockchainSdkConfig = createBlockchainSdkConfig(),
|
||||||
|
amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey,
|
||||||
|
appsFlyerApiKey = AppsFlyer.appsFlyerDevKey,
|
||||||
|
appsAppId = AppsFlyer.appsFlyerAppID,
|
||||||
|
walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId,
|
||||||
|
express = createExpressModel(
|
||||||
|
apiKey = Express.apiKey,
|
||||||
|
signVerifierPublicKey = Express.signVerifierPublicKey,
|
||||||
|
),
|
||||||
|
devExpress = createExpressModel(
|
||||||
|
apiKey = DevExpress.apiKey,
|
||||||
|
signVerifierPublicKey = DevExpress.signVerifierPublicKey,
|
||||||
|
),
|
||||||
|
stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey,
|
||||||
|
p2pApiKey = createP2PKeys(),
|
||||||
|
blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey,
|
||||||
|
tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey,
|
||||||
|
tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev,
|
||||||
|
tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage,
|
||||||
|
yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey,
|
||||||
|
yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev,
|
||||||
|
bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken,
|
||||||
|
bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev,
|
||||||
|
gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev,
|
||||||
|
gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? {
|
||||||
|
return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) {
|
||||||
|
ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createP2PKeys(): P2PKeys? {
|
||||||
|
val mainnet = P2pApiKey.mainnet
|
||||||
|
val hoodi = P2pApiKey.hoodi
|
||||||
|
return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) {
|
||||||
|
P2PKeys(mainnet = mainnet, hoodi = hoodi)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBlockchainSdkConfig(): BlockchainSdkConfig {
|
||||||
|
return BlockchainSdkConfig(
|
||||||
|
blockchairCredentials = BlockchairCredentials(
|
||||||
|
apiKey = GeneratedEnvironmentConfig.blockchairApiKeys,
|
||||||
|
authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken,
|
||||||
|
),
|
||||||
|
blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(),
|
||||||
|
quickNodeSolanaCredentials = QuickNodeCredentials(
|
||||||
|
apiKey = GeneratedEnvironmentConfig.quiknodeApiKey,
|
||||||
|
subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain,
|
||||||
|
),
|
||||||
|
quickNodeBscCredentials = QuickNodeCredentials(
|
||||||
|
apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey,
|
||||||
|
subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain,
|
||||||
|
),
|
||||||
|
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||||
|
apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey,
|
||||||
|
subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain,
|
||||||
|
),
|
||||||
|
quickNodeMonadCredentials = QuickNodeCredentials(
|
||||||
|
apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey,
|
||||||
|
subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain,
|
||||||
|
),
|
||||||
|
infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId,
|
||||||
|
tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey,
|
||||||
|
nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey),
|
||||||
|
getBlockCredentials = createGetBlockCredentials(),
|
||||||
|
kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl,
|
||||||
|
tonCenterCredentials = TonCenterCredentials(
|
||||||
|
mainnetApiKey = TonCenterApiKey.mainnet,
|
||||||
|
testnetApiKey = TonCenterApiKey.testnet,
|
||||||
|
),
|
||||||
|
chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey,
|
||||||
|
chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey,
|
||||||
|
hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey,
|
||||||
|
polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey,
|
||||||
|
bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey,
|
||||||
|
bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey,
|
||||||
|
dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey,
|
||||||
|
koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey,
|
||||||
|
alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey,
|
||||||
|
moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey,
|
||||||
|
etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey,
|
||||||
|
blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey,
|
||||||
|
tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGetBlockCredentials(): GetBlockCredentials {
|
||||||
|
return GetBlockCredentials(
|
||||||
|
xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc),
|
||||||
|
cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta),
|
||||||
|
avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc),
|
||||||
|
eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc),
|
||||||
|
etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc),
|
||||||
|
fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc),
|
||||||
|
rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc),
|
||||||
|
bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc),
|
||||||
|
polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc),
|
||||||
|
gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc),
|
||||||
|
cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc),
|
||||||
|
solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc),
|
||||||
|
ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc),
|
||||||
|
tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest),
|
||||||
|
cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest),
|
||||||
|
near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc),
|
||||||
|
aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest),
|
||||||
|
dogecoin = GetBlockAccessToken(
|
||||||
|
jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc,
|
||||||
|
blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest,
|
||||||
|
),
|
||||||
|
litecoin = GetBlockAccessToken(
|
||||||
|
jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc,
|
||||||
|
blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest,
|
||||||
|
),
|
||||||
|
dash = GetBlockAccessToken(
|
||||||
|
jsonRpc = GetBlockAccessTokens.Dash.jsonRpc,
|
||||||
|
blockBookRest = GetBlockAccessTokens.Dash.blockBookRest,
|
||||||
|
),
|
||||||
|
bitcoin = GetBlockAccessToken(
|
||||||
|
jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc,
|
||||||
|
blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest,
|
||||||
|
),
|
||||||
|
algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest),
|
||||||
|
zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc),
|
||||||
|
polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc),
|
||||||
|
base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc),
|
||||||
|
blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc),
|
||||||
|
filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc),
|
||||||
|
arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc),
|
||||||
|
bitcoinCash = GetBlockAccessToken(
|
||||||
|
jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc,
|
||||||
|
blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest,
|
||||||
|
),
|
||||||
|
kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc),
|
||||||
|
moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc),
|
||||||
|
optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc),
|
||||||
|
polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc),
|
||||||
|
shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc),
|
||||||
|
sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc),
|
||||||
|
telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc),
|
||||||
|
tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest),
|
||||||
|
monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest),
|
||||||
|
stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
@file:Suppress("BooleanPropertyNaming")
|
||||||
|
package com.tangem.datasource.local.visa.entity
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.tangem.domain.models.kyc.KycStatus
|
||||||
|
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
|
||||||
|
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment account status for storage in the local cache.
|
||||||
|
*
|
||||||
|
* @see [com.tangem.domain.pay.PaymentAccountStatus]
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||||
|
sealed interface PaymentAccountStatusDM {
|
||||||
|
|
||||||
|
@NameLabel("not_created")
|
||||||
|
data class NotCreated(
|
||||||
|
@Json(name = "not_created") val marker: Boolean = true,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
|
||||||
|
@NameLabel("kyc_status")
|
||||||
|
data class UnderReview(
|
||||||
|
@Json(name = "kyc_status") val kycStatus: KycStatus,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
|
||||||
|
@NameLabel("issuing_card")
|
||||||
|
data class IssuingCard(
|
||||||
|
@Json(name = "issuing_card") val marker: Boolean = true,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
|
||||||
|
@NameLabel("locked")
|
||||||
|
data class Locked(
|
||||||
|
@Json(name = "locked") val marker: Boolean = true,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
|
||||||
|
@NameLabel("balance")
|
||||||
|
data class Loaded(
|
||||||
|
@Json(name = "card_id") val cardId: String,
|
||||||
|
@Json(name = "last_four_digits") val lastFourDigits: String,
|
||||||
|
@Json(name = "balance") val balance: BigDecimal,
|
||||||
|
@Json(name = "currency_code") val currencyCode: String,
|
||||||
|
@Json(name = "deposit_address") val depositAddress: String?,
|
||||||
|
@Json(name = "is_pin_set") val isPinSet: Boolean,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
|
||||||
|
@NameLabel("card_issue_failed")
|
||||||
|
data class CardIssueFailed(
|
||||||
|
@Json(name = "card_issue_failed") val marker: Boolean = true,
|
||||||
|
) : PaymentAccountStatusDM
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -19,6 +20,7 @@ class ApiConfigTest {
|
||||||
|
|
||||||
private val appAuthProvider = mockk<AuthProvider>()
|
private val appAuthProvider = mockk<AuthProvider>()
|
||||||
private val apiKeyProvider = mockk<ProviderSuspend<String>>()
|
private val apiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||||
|
private val environmentConfig = mockk<EnvironmentConfig>()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
@ -47,7 +49,7 @@ class ApiConfigTest {
|
||||||
when (it) {
|
when (it) {
|
||||||
ApiConfig.ID.Express -> {
|
ApiConfig.ID.Express -> {
|
||||||
Express(
|
Express(
|
||||||
environmentConfigStorage = mockk(),
|
environmentConfig = environmentConfig,
|
||||||
expressAuthProvider = mockk(),
|
expressAuthProvider = mockk(),
|
||||||
appVersionProvider = mockk(),
|
appVersionProvider = mockk(),
|
||||||
appInfoProvider = mockk(),
|
appInfoProvider = mockk(),
|
||||||
|
|
@ -55,7 +57,7 @@ class ApiConfigTest {
|
||||||
}
|
}
|
||||||
ApiConfig.ID.YieldSupply -> {
|
ApiConfig.ID.YieldSupply -> {
|
||||||
YieldSupply(
|
YieldSupply(
|
||||||
environmentConfigStorage = mockk(),
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = mockk(),
|
appVersionProvider = mockk(),
|
||||||
authProvider = appAuthProvider,
|
authProvider = appAuthProvider,
|
||||||
appInfoProvider = mockk(),
|
appInfoProvider = mockk(),
|
||||||
|
|
@ -70,14 +72,14 @@ class ApiConfigTest {
|
||||||
}
|
}
|
||||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||||
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = mockk(),
|
appVersionProvider = mockk(),
|
||||||
environmentConfigStorage = mockk()
|
|
||||||
)
|
)
|
||||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||||
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = mockk(),
|
appVersionProvider = mockk(),
|
||||||
environmentConfigStorage = mockk()
|
|
||||||
)
|
)
|
||||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
|
||||||
ApiConfig.ID.MoonPay -> MoonPay()
|
ApiConfig.ID.MoonPay -> MoonPay()
|
||||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||||
ApiConfig.ID.News -> News(
|
ApiConfig.ID.News -> News(
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
package com.tangem.datasource.api.common.config.managers
|
|
||||||
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
|
||||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest]
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
|
||||||
|
|
||||||
private val environmentConfig = EnvironmentConfig(
|
|
||||||
express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"),
|
|
||||||
devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"),
|
|
||||||
blockAidApiKey = BLOCK_AID_API_KEY,
|
|
||||||
tangemApiKey = TANGEM_API_KEY,
|
|
||||||
tangemApiKeyDev = TANGEM_API_KEY_DEV,
|
|
||||||
bffStaticToken = TANGEM_PAY_BFF_KEY,
|
|
||||||
bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
|
|
||||||
tangemApiKeyStage = TANGEM_API_KEY_STAGE,
|
|
||||||
yieldModuleApiKey = YIELD_MODULE_KEY,
|
|
||||||
yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun initialize() = environmentConfig
|
|
||||||
override fun getConfig() = flowOf(environmentConfig)
|
|
||||||
override fun getConfigSync() = environmentConfig
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val EXPRESS_API_KEY = "express_api_key"
|
|
||||||
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
|
|
||||||
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
|
||||||
const val TANGEM_API_KEY = "tangem_api_key"
|
|
||||||
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
|
|
||||||
const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key"
|
|
||||||
const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
|
|
||||||
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
|
|
||||||
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
|
|
||||||
const val YIELD_MODULE_KEY = "yield_module_api_key"
|
|
||||||
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
|
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY
|
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV
|
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
|
|
@ -39,7 +37,7 @@ import java.util.TimeZone
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
internal class ProdApiConfigsManagerTest {
|
internal class ProdApiConfigsManagerTest {
|
||||||
|
|
||||||
private val environmentConfigStorage = MockEnvironmentConfigStorage()
|
private val environmentConfig = createMockEnvironmentConfig()
|
||||||
private val appVersionProvider = mockk<AppVersionProvider>()
|
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||||
|
|
@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest {
|
||||||
when (it) {
|
when (it) {
|
||||||
ApiConfig.ID.Express -> {
|
ApiConfig.ID.Express -> {
|
||||||
Express(
|
Express(
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfig = environmentConfig,
|
||||||
expressAuthProvider = expressAuthProvider,
|
expressAuthProvider = expressAuthProvider,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
appInfoProvider = appInfoProvider,
|
appInfoProvider = appInfoProvider,
|
||||||
|
|
@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest {
|
||||||
}
|
}
|
||||||
ApiConfig.ID.YieldSupply -> {
|
ApiConfig.ID.YieldSupply -> {
|
||||||
YieldSupply(
|
YieldSupply(
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
authProvider = appAuthProvider,
|
authProvider = appAuthProvider,
|
||||||
appInfoProvider = appInfoProvider,
|
appInfoProvider = appInfoProvider,
|
||||||
|
|
@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest {
|
||||||
}
|
}
|
||||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
||||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||||
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
|
||||||
)
|
)
|
||||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||||
|
environmentConfig = environmentConfig,
|
||||||
appVersionProvider = appVersionProvider,
|
appVersionProvider = appVersionProvider,
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
|
||||||
)
|
)
|
||||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
|
||||||
ApiConfig.ID.MoonPay -> MoonPay()
|
ApiConfig.ID.MoonPay -> MoonPay()
|
||||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||||
ApiConfig.ID.News -> News(
|
ApiConfig.ID.News -> News(
|
||||||
|
|
@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest {
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"api-key" to ProviderSuspend {
|
"api-key" to ProviderSuspend {
|
||||||
if (environment == ApiEnvironment.PROD) {
|
if (environment == ApiEnvironment.PROD) {
|
||||||
MockEnvironmentConfigStorage.EXPRESS_API_KEY
|
EXPRESS_API_KEY
|
||||||
} else {
|
} else {
|
||||||
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
|
EXPRESS_DEV_API_KEY
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
|
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
|
||||||
|
|
@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest {
|
||||||
environment = ApiEnvironment.PROD,
|
environment = ApiEnvironment.PROD,
|
||||||
baseUrl = "https://yield.tangem.org/",
|
baseUrl = "https://yield.tangem.org/",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY },
|
"api-key" to ProviderSuspend { YIELD_MODULE_KEY },
|
||||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||||
"version" to ProviderSuspend { VERSION_NAME },
|
"version" to ProviderSuspend { VERSION_NAME },
|
||||||
|
|
@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest {
|
||||||
const val P2P_API_KEY = "p2p_api_key"
|
const val P2P_API_KEY = "p2p_api_key"
|
||||||
const val APP_CARD_ID = "app_card_id"
|
const val APP_CARD_ID = "app_card_id"
|
||||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||||
|
|
||||||
|
// Mock config values
|
||||||
|
const val TANGEM_API_KEY = "tangem_api_key"
|
||||||
|
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
|
||||||
|
const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
|
||||||
|
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
||||||
|
const val EXPRESS_API_KEY = "express_api_key"
|
||||||
|
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
|
||||||
|
const val YIELD_MODULE_KEY = "yield_module_key"
|
||||||
|
|
||||||
|
fun createMockEnvironmentConfig(): EnvironmentConfig {
|
||||||
|
return EnvironmentConfig(
|
||||||
|
moonPayApiKey = "moon_pay_api_key",
|
||||||
|
moonPayApiSecretKey = "moon_pay_secret_key",
|
||||||
|
mercuryoWidgetId = "mercuryo_widget_id",
|
||||||
|
mercuryoSecret = "mercuryo_secret",
|
||||||
|
blockchainSdkConfig = mockk(relaxed = true),
|
||||||
|
amplitudeApiKey = "amplitude_api_key",
|
||||||
|
appsFlyerApiKey = "appsflyer_api_key",
|
||||||
|
appsAppId = "apps_app_id",
|
||||||
|
walletConnectProjectId = "wallet_connect_project_id",
|
||||||
|
express = ExpressModel(
|
||||||
|
apiKey = EXPRESS_API_KEY,
|
||||||
|
signVerifierPublicKey = "express_public_key",
|
||||||
|
),
|
||||||
|
devExpress = ExpressModel(
|
||||||
|
apiKey = EXPRESS_DEV_API_KEY,
|
||||||
|
signVerifierPublicKey = "express_dev_public_key",
|
||||||
|
),
|
||||||
|
stakeKitApiKey = STAKE_KIT_API_KEY,
|
||||||
|
p2pApiKey = null,
|
||||||
|
blockAidApiKey = BLOCK_AID_API_KEY,
|
||||||
|
tangemApiKey = TANGEM_API_KEY,
|
||||||
|
tangemApiKeyDev = TANGEM_API_KEY,
|
||||||
|
tangemApiKeyStage = TANGEM_API_KEY,
|
||||||
|
yieldModuleApiKey = YIELD_MODULE_KEY,
|
||||||
|
yieldModuleApiKeyDev = YIELD_MODULE_KEY,
|
||||||
|
bffStaticToken = TANGEM_PAY_BFF_KEY_DEV,
|
||||||
|
bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
|
||||||
|
gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY,
|
||||||
|
gaslessTxApiKey = TANGEM_GASLESS_API_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,12 +61,12 @@ dependencies {
|
||||||
api(deps.jodatime)
|
api(deps.jodatime)
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
implementation(deps.markdown)
|
implementation(deps.markdown)
|
||||||
implementation(deps.haze) {
|
api(deps.haze) {
|
||||||
exclude(module = "activity-compose")
|
exclude(module = "activity-compose")
|
||||||
exclude(module = "activity")
|
exclude(module = "activity")
|
||||||
exclude(module = "activity-ktx")
|
exclude(module = "activity-ktx")
|
||||||
}
|
}
|
||||||
implementation(deps.haze.materials) {
|
api(deps.haze.materials) {
|
||||||
exclude(module = "activity-compose")
|
exclude(module = "activity-compose")
|
||||||
exclude(module = "activity")
|
exclude(module = "activity")
|
||||||
exclude(module = "activity-ktx")
|
exclude(module = "activity-ktx")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
@file:Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||||
|
package com.tangem.core.ui.components.background
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import com.tangem.core.ui.shader.TangemShader
|
||||||
|
import com.tangem.core.ui.shader.runtime.buildEffect
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.shaderBackground(
|
||||||
|
shader: TangemShader,
|
||||||
|
speed: Float = 1f,
|
||||||
|
fallback: () -> Brush = {
|
||||||
|
Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||||
|
},
|
||||||
|
): Modifier {
|
||||||
|
val runtimeEffect = remember(shader) { buildEffect(shader) }
|
||||||
|
var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
|
||||||
|
val speedModifier = shader.speedModifier
|
||||||
|
|
||||||
|
val time by if (runtimeEffect.isSupported) {
|
||||||
|
var startMillis = remember(shader) { -1L }
|
||||||
|
produceState(0f, speedModifier) {
|
||||||
|
while (true) {
|
||||||
|
withInfiniteAnimationFrameMillis { frameTimeMillis ->
|
||||||
|
if (startMillis < 0) startMillis = frameTimeMillis
|
||||||
|
value = ((frameTimeMillis - startMillis) / 16.6f) / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remember { mutableFloatStateOf(-1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return this then Modifier.onGloballyPositioned {
|
||||||
|
size = Size(it.size.width.toFloat(), it.size.height.toFloat())
|
||||||
|
}.drawBehind {
|
||||||
|
runtimeEffect.update(
|
||||||
|
shader = shader,
|
||||||
|
time = (time * speed * speedModifier).round(3),
|
||||||
|
width = size.width,
|
||||||
|
height = size.height,
|
||||||
|
) // set uniforms for the shaders
|
||||||
|
|
||||||
|
if (runtimeEffect.isReady) {
|
||||||
|
drawRect(brush = runtimeEffect.build())
|
||||||
|
} else {
|
||||||
|
drawRect(brush = fallback())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Float.round(decimals: Int): Float {
|
||||||
|
var multiplier = 1.0f
|
||||||
|
repeat(decimals) { multiplier *= 10 }
|
||||||
|
return round(this * multiplier) / multiplier
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
package com.tangem.core.ui.components.background.northernlights
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import android.graphics.BlurMaskFilter
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Paint
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
|
||||||
|
|
||||||
|
// ── Circle 1 (left) ──────────────────────────────────────────────────────
|
||||||
|
val color1 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF3355EE),
|
||||||
|
targetValue = Color(0xFF5577FF),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(4_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "color1",
|
||||||
|
)
|
||||||
|
val x1 by transition.animateFloat(
|
||||||
|
initialValue = 0.05f,
|
||||||
|
targetValue = 0.28f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(5_500, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "x1",
|
||||||
|
)
|
||||||
|
val y1 by transition.animateFloat(
|
||||||
|
initialValue = 0.0f,
|
||||||
|
targetValue = 0.18f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "y1",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Circle 2 (right) ─────────────────────────────────────────────────────
|
||||||
|
val color2 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF7733CC),
|
||||||
|
targetValue = Color(0xFF4455EE),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(1_500),
|
||||||
|
),
|
||||||
|
label = "color2",
|
||||||
|
)
|
||||||
|
val x2 by transition.animateFloat(
|
||||||
|
initialValue = 0.68f,
|
||||||
|
targetValue = 0.92f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "x2",
|
||||||
|
)
|
||||||
|
val y2 by transition.animateFloat(
|
||||||
|
initialValue = 0.02f,
|
||||||
|
targetValue = 0.20f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(2_000),
|
||||||
|
),
|
||||||
|
label = "y2",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Oval (center) ────────────────────────────────────────────────────────
|
||||||
|
val ovalColor by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF5533CC),
|
||||||
|
targetValue = Color(0xFF8844EE),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(2_500),
|
||||||
|
),
|
||||||
|
label = "ovalColor",
|
||||||
|
)
|
||||||
|
// ── Circle 3 (center) ────────────────────────────────────────────────────
|
||||||
|
val color3 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF9933BB),
|
||||||
|
targetValue = Color(0xFFBB44DD),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(3_000),
|
||||||
|
),
|
||||||
|
label = "color3",
|
||||||
|
)
|
||||||
|
val x3 by transition.animateFloat(
|
||||||
|
initialValue = 0.35f,
|
||||||
|
targetValue = 0.58f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(6_500, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(1_000),
|
||||||
|
),
|
||||||
|
label = "x3",
|
||||||
|
)
|
||||||
|
val y3 by transition.animateFloat(
|
||||||
|
initialValue = 0.0f,
|
||||||
|
targetValue = 0.15f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(4_500, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
initialStartOffset = StartOffset(500),
|
||||||
|
),
|
||||||
|
label = "y3",
|
||||||
|
)
|
||||||
|
|
||||||
|
var blurRadiusState by remember { mutableFloatStateOf(0f) }
|
||||||
|
val circlePaint1 = remember { Paint() }
|
||||||
|
val circlePaint2 = remember { Paint() }
|
||||||
|
val circlePaint3 = remember { Paint() }
|
||||||
|
val ovalPaint = remember { Paint() }
|
||||||
|
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f)
|
||||||
|
val circleRadius = size.width * 0.52f
|
||||||
|
|
||||||
|
// Update maskFilter only when blur radius changes meaningfully
|
||||||
|
if (blurRadiusState != blurRadius) {
|
||||||
|
blurRadiusState = blurRadius
|
||||||
|
val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
|
||||||
|
circlePaint1.asFrameworkPaint().maskFilter = mf
|
||||||
|
circlePaint2.asFrameworkPaint().maskFilter = mf
|
||||||
|
circlePaint3.asFrameworkPaint().maskFilter = mf
|
||||||
|
ovalPaint.asFrameworkPaint().maskFilter = mf
|
||||||
|
}
|
||||||
|
|
||||||
|
circlePaint1.color = color1.copy(alpha = 0.85f)
|
||||||
|
circlePaint2.color = color2.copy(alpha = 0.85f)
|
||||||
|
circlePaint3.color = color3.copy(alpha = 0.85f)
|
||||||
|
ovalPaint.color = ovalColor.copy(alpha = 0.80f)
|
||||||
|
|
||||||
|
drawIntoCanvas { canvas ->
|
||||||
|
canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1)
|
||||||
|
canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2)
|
||||||
|
canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3)
|
||||||
|
|
||||||
|
val halfW = size.width * 0.68f
|
||||||
|
val halfH = size.width * 0.24f
|
||||||
|
val ovalCx = size.width * 0.50f
|
||||||
|
val ovalCy = 0f
|
||||||
|
|
||||||
|
canvas.drawOval(
|
||||||
|
Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH),
|
||||||
|
ovalPaint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.components.background.northernlights
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.StartOffset
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.keyframes
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.tangem.core.ui.components.background.shaderBackground
|
||||||
|
import com.tangem.core.ui.res.LocalPowerSavingState
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated northern lights background.
|
||||||
|
* Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) {
|
||||||
|
val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||||
|
if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
|
||||||
|
NorthernLightsBackgroundWithShader(modifier)
|
||||||
|
} else {
|
||||||
|
MovingColorfulBlubsBackground(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
|
||||||
|
val backgroundColor = TangemTheme.colors2.surface.level1
|
||||||
|
|
||||||
|
// Each track cycles through 4 states (matching the screenshot frames):
|
||||||
|
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
|
||||||
|
// 16 s total per track, staggered so no two tracks peak simultaneously.
|
||||||
|
|
||||||
|
// ── Color 1 – indigo → bright blue → lavender → hot violet ──────────────
|
||||||
|
val color1 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF2A1480),
|
||||||
|
targetValue = Color(0xFF2A1480),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = 16_000
|
||||||
|
Color(0xFF2A1480) at 0 using FastOutSlowInEasing
|
||||||
|
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
),
|
||||||
|
label = "color1",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Color 2 – dark blue → cyan-blue → sky → teal ─────────────────────────
|
||||||
|
val color2 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF1444AA),
|
||||||
|
targetValue = Color(0xFF1444AA),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = 16_000
|
||||||
|
Color(0xFF1444AA) at 0 using FastOutSlowInEasing
|
||||||
|
Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
initialStartOffset = StartOffset(4_000),
|
||||||
|
),
|
||||||
|
label = "color2",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Color 3 – dark purple → medium purple → rose pink → magenta ──────────
|
||||||
|
val color3 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF4422BB),
|
||||||
|
targetValue = Color(0xFF4422BB),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = 16_000
|
||||||
|
Color(0xFF4422BB) at 0 using FastOutSlowInEasing
|
||||||
|
Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
initialStartOffset = StartOffset(8_000),
|
||||||
|
),
|
||||||
|
label = "color3",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Color 4 – dark violet → medium violet → light pink → hot pink ────────
|
||||||
|
val color4 by transition.animateColor(
|
||||||
|
initialValue = Color(0xFF331199),
|
||||||
|
targetValue = Color(0xFF331199),
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = 16_000
|
||||||
|
Color(0xFF331199) at 0 using FastOutSlowInEasing
|
||||||
|
Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing
|
||||||
|
Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
initialStartOffset = StartOffset(2_000),
|
||||||
|
),
|
||||||
|
label = "color4",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep a stable shader instance so the RuntimeShader is never recreated.
|
||||||
|
// Colors are pushed each recomposition via updateColors().
|
||||||
|
val shader = remember {
|
||||||
|
NorthernLightsMeshGradientShader(
|
||||||
|
colors = arrayOf(
|
||||||
|
Color(0xFF2A1480),
|
||||||
|
Color(0xFF1444AA),
|
||||||
|
Color(0xFF4422BB),
|
||||||
|
Color(0xFF331199),
|
||||||
|
backgroundColor,
|
||||||
|
),
|
||||||
|
speed = 0.5f,
|
||||||
|
scale = 4f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val colorsArray = remember { Array(5) { Color.Unspecified } }
|
||||||
|
colorsArray[0] = color1
|
||||||
|
colorsArray[1] = color2
|
||||||
|
colorsArray[2] = color3
|
||||||
|
colorsArray[3] = color4
|
||||||
|
colorsArray[4] = backgroundColor
|
||||||
|
shader.updateColors(colorsArray)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.shaderBackground(shader),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
||||||
fun TangemPullToRefreshContainer(
|
fun TangemPullToRefreshContainer(
|
||||||
config: PullToRefreshConfig,
|
config: PullToRefreshConfig,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
indicatorModifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val state = rememberPullToRefreshState()
|
val state = rememberPullToRefreshState()
|
||||||
|
|
@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
indicator = {
|
indicator = {
|
||||||
Indicator(
|
Indicator(
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
modifier = indicatorModifier.align(Alignment.TopCenter),
|
||||||
isRefreshing = config.isRefreshing,
|
isRefreshing = config.isRefreshing,
|
||||||
state = state,
|
state = state,
|
||||||
containerColor = TangemTheme.colors.background.tertiary,
|
containerColor = TangemTheme.colors.background.tertiary,
|
||||||
|
|
|
||||||
|
|
@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle {
|
||||||
override fun createShader(size: Size): Shader {
|
override fun createShader(size: Size): Shader {
|
||||||
val center = Offset(size.width / 2f, size.height / 2f)
|
val center = Offset(size.width / 2f, size.height / 2f)
|
||||||
val diagonal = sqrt(size.width * size.width + size.height * size.height)
|
val diagonal = sqrt(size.width * size.width + size.height * size.height)
|
||||||
val direction = Offset(x = 1f, y = 0.5f)
|
// Subtle diagonal angle, similar to iOS shimmer
|
||||||
val halfDist = diagonal / 2f
|
val direction = Offset(x = 1f, y = 0.3f)
|
||||||
val baseStart = center - direction * halfDist
|
|
||||||
val baseEnd = center + direction * halfDist
|
|
||||||
val shift = direction * offset * diagonal
|
|
||||||
|
|
||||||
|
// Half-width of the blob (80% of diagonal total — wide, soft sweep)
|
||||||
|
val bandHalf = diagonal * 0.40f
|
||||||
|
|
||||||
|
// Sweep the highlight center from left-of-element to right-of-element.
|
||||||
|
// offset 0..1 maps to a full pass including off-screen padding on both sides.
|
||||||
|
val shift = direction * ((offset - 0.5f) * diagonal * 1.5f)
|
||||||
|
val highlightCenter = center + shift
|
||||||
|
|
||||||
|
// Full color text with a wide, gradual low-alpha dip sweeping left → right
|
||||||
return LinearGradientShader(
|
return LinearGradientShader(
|
||||||
colors = listOf(textColor.copy(alpha = 0.2f), textColor),
|
colors = listOf(
|
||||||
from = baseStart + shift,
|
textColor,
|
||||||
to = baseEnd + shift,
|
textColor.copy(alpha = 0.75f),
|
||||||
colorStops = listOf(0.0f, 0.15f),
|
textColor.copy(alpha = 0.45f),
|
||||||
tileMode = TileMode.Mirror,
|
textColor.copy(alpha = 0.3f),
|
||||||
|
textColor.copy(alpha = 0.45f),
|
||||||
|
textColor.copy(alpha = 0.75f),
|
||||||
|
textColor,
|
||||||
|
),
|
||||||
|
from = highlightCenter - direction * bandHalf,
|
||||||
|
to = highlightCenter + direction * bandHalf,
|
||||||
|
colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f),
|
||||||
|
tileMode = TileMode.Clamp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
package com.tangem.core.ui.ds
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val ANIMATION_DURATION = 300
|
||||||
|
private const val MAX_VISIBLE_DOTS = 5
|
||||||
|
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
|
||||||
|
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
|
||||||
|
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
|
||||||
|
|
||||||
|
private val SPACING = 4.dp
|
||||||
|
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
|
||||||
|
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
|
||||||
|
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||||
|
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation.
|
||||||
|
*
|
||||||
|
* A pager indicator that adapts to the number of pages and the current page index.
|
||||||
|
*
|
||||||
|
* For 5 or fewer pages, it shows all dots with the current page highlighted.
|
||||||
|
* For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position.
|
||||||
|
*
|
||||||
|
* @param pagerState state of the pager to observe
|
||||||
|
* @param activeIndicatorColor color for the active page indicator
|
||||||
|
* @param inactiveIndicatorColor color for the inactive page indicators
|
||||||
|
* @param modifier modifier for styling
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
|
@Composable
|
||||||
|
fun TangemPagerIndicator(
|
||||||
|
pagerState: PagerState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary,
|
||||||
|
inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||||
|
) {
|
||||||
|
val totalPages = pagerState.pageCount
|
||||||
|
val currentIndex = pagerState.currentPage
|
||||||
|
|
||||||
|
if (totalPages == 0) return
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
|
||||||
|
|
||||||
|
var displayLower by remember { mutableIntStateOf(targetLower) }
|
||||||
|
var displayUpper by remember { mutableIntStateOf(targetUpper) }
|
||||||
|
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
|
||||||
|
|
||||||
|
val slideOffset = remember { Animatable(0f) }
|
||||||
|
var isSliding by remember { mutableStateOf(false) }
|
||||||
|
var slideDirection by remember { mutableIntStateOf(0) }
|
||||||
|
val fadeProgress = remember { Animatable(0f) }
|
||||||
|
var fadeJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(targetLower) {
|
||||||
|
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
|
||||||
|
fadeJob?.cancel()
|
||||||
|
slideOffset.stop()
|
||||||
|
fadeProgress.stop()
|
||||||
|
|
||||||
|
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||||
|
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||||
|
val halfEdge = edgeDotSize / 2
|
||||||
|
|
||||||
|
isSliding = true
|
||||||
|
slideDirection = dir
|
||||||
|
fadeProgress.snapTo(0f)
|
||||||
|
|
||||||
|
if (dir > 0) {
|
||||||
|
displayLower = prevTargetLower
|
||||||
|
displayUpper = targetUpper
|
||||||
|
slideOffset.snapTo(halfEdge)
|
||||||
|
} else {
|
||||||
|
displayLower = targetLower
|
||||||
|
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||||
|
slideOffset.snapTo(-halfEdge)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevTargetLower = targetLower
|
||||||
|
|
||||||
|
fadeJob = launch {
|
||||||
|
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||||
|
}
|
||||||
|
slideOffset.animateTo(
|
||||||
|
if (dir > 0) -halfEdge else halfEdge,
|
||||||
|
tween(ANIMATION_DURATION),
|
||||||
|
)
|
||||||
|
|
||||||
|
displayLower = targetLower
|
||||||
|
displayUpper = targetUpper
|
||||||
|
slideOffset.snapTo(0f)
|
||||||
|
isSliding = false
|
||||||
|
slideDirection = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val visibleIndices = (displayLower until displayUpper).toList()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.offset {
|
||||||
|
IntOffset(slideOffset.value.roundToInt(), 0)
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(SPACING),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
visibleIndices.forEach { index ->
|
||||||
|
val dotAlpha = when {
|
||||||
|
!isSliding -> 1f
|
||||||
|
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
|
||||||
|
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
|
||||||
|
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
|
||||||
|
slideDirection < 0 && index == displayLower -> fadeProgress.value
|
||||||
|
else -> 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
key(index) {
|
||||||
|
Dot(
|
||||||
|
index = index,
|
||||||
|
currentIndex = currentIndex,
|
||||||
|
totalPages = totalPages,
|
||||||
|
activeColor = activeIndicatorColor,
|
||||||
|
inactiveColor = inactiveIndicatorColor,
|
||||||
|
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||||
|
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||||
|
return 0 to totalPages
|
||||||
|
}
|
||||||
|
val lowerBound = when {
|
||||||
|
currentIndex <= 1 -> 0
|
||||||
|
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||||
|
else -> currentIndex - 2
|
||||||
|
}
|
||||||
|
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||||
|
return lowerBound to upperBound
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
||||||
|
if (index == currentIndex) {
|
||||||
|
return CURRENT_DOT_SIZE
|
||||||
|
}
|
||||||
|
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||||
|
return NORMAL_DOT_SIZE
|
||||||
|
}
|
||||||
|
val params = DotSizeParams.create(index, currentIndex, totalPages)
|
||||||
|
return params.calculateSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DotSizeParams private constructor(
|
||||||
|
val posInWindow: Int,
|
||||||
|
val currentPosInWindow: Int,
|
||||||
|
val hiddenLeft: Int,
|
||||||
|
val hiddenRight: Int,
|
||||||
|
val distanceFromCurrent: Int,
|
||||||
|
) {
|
||||||
|
private val lastPos = MAX_VISIBLE_DOTS - 1
|
||||||
|
private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
|
||||||
|
|
||||||
|
fun calculateSize(): DpSize = when {
|
||||||
|
isCentered -> getCenteredSize()
|
||||||
|
hiddenRight >= 1 -> getRightEdgeSize()
|
||||||
|
hiddenLeft >= 1 -> getLeftEdgeSize()
|
||||||
|
else -> NORMAL_DOT_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenteredSize(): DpSize = when (posInWindow) {
|
||||||
|
0, lastPos -> HINT_DOT_SIZE
|
||||||
|
else -> NORMAL_DOT_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRightEdgeSize(): DpSize {
|
||||||
|
val isLastPos = posInWindow == lastPos
|
||||||
|
val isSecondToLast = posInWindow == lastPos - 1
|
||||||
|
val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||||
|
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||||
|
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||||
|
isLastPos && isModerateDistance -> HINT_DOT_SIZE
|
||||||
|
isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||||
|
else -> NORMAL_DOT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLeftEdgeSize(): DpSize {
|
||||||
|
val isFirstPos = posInWindow == 0
|
||||||
|
val isSecondPos = posInWindow == 1
|
||||||
|
val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||||
|
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||||
|
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||||
|
isFirstPos && isModerateDistance -> HINT_DOT_SIZE
|
||||||
|
isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||||
|
else -> NORMAL_DOT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
|
||||||
|
val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
|
||||||
|
val posInWindow = index - windowStart
|
||||||
|
val currentPosInWindow = currentIndex - windowStart
|
||||||
|
return DotSizeParams(
|
||||||
|
posInWindow = posInWindow,
|
||||||
|
currentPosInWindow = currentPosInWindow,
|
||||||
|
hiddenLeft = windowStart,
|
||||||
|
hiddenRight = totalPages - windowEnd,
|
||||||
|
distanceFromCurrent = abs(posInWindow - currentPosInWindow),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Dot(
|
||||||
|
index: Int,
|
||||||
|
currentIndex: Int,
|
||||||
|
totalPages: Int,
|
||||||
|
activeColor: Color,
|
||||||
|
inactiveColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val isActive = index == currentIndex
|
||||||
|
val size = getDotSize(index, currentIndex, totalPages)
|
||||||
|
|
||||||
|
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||||
|
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||||
|
|
||||||
|
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||||
|
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||||
|
val animatedColor by animateColorAsState(
|
||||||
|
targetValue = if (isActive) activeColor else inactiveColor,
|
||||||
|
animationSpec = colorSpec,
|
||||||
|
label = "c$index",
|
||||||
|
)
|
||||||
|
|
||||||
|
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.width(animatedWidth)
|
||||||
|
.height(animatedHeight)
|
||||||
|
.background(animatedColor, shape),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PagerIndicatorPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
listOf(0, 1, 2, 3, 4).forEach { page ->
|
||||||
|
TangemPagerIndicator(rememberPagerState(page) { 5 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PagerIndicator6ItemsPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
listOf(0, 1, 2, 3, 4, 5).forEach { page ->
|
||||||
|
TangemPagerIndicator(rememberPagerState(page) { 6 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PagerIndicator7ItemsPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
|
||||||
|
TangemPagerIndicator(rememberPagerState(page) { 7 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PagerIndicator10ItemsPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
|
||||||
|
TangemPagerIndicator(rememberPagerState(page) { 10 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PagerIndicatorSmallCountsPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
TangemPagerIndicator(rememberPagerState(0) { 1 })
|
||||||
|
TangemPagerIndicator(rememberPagerState(1) { 2 })
|
||||||
|
TangemPagerIndicator(rememberPagerState(1) { 3 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -72,14 +72,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun TangemBadge(
|
fun TangemBadge(
|
||||||
text: TextReference,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
text: TextReference? = null,
|
||||||
@DrawableRes iconRes: Int? = null,
|
@DrawableRes iconRes: Int? = null,
|
||||||
size: TangemBadgeSize = X9,
|
size: TangemBadgeSize = X9,
|
||||||
shape: TangemBadgeShape = TangemBadgeShape.Default,
|
shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||||
color: TangemBadgeColor = TangemBadgeColor.Gray,
|
color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||||
type: TangemBadgeType = TangemBadgeType.Solid,
|
type: TangemBadgeType = TangemBadgeType.Solid,
|
||||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
|
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val iconColor = getIconColor(type = type, color = color)
|
val iconColor = getIconColor(type = type, color = color)
|
||||||
|
|
@ -94,7 +94,7 @@ fun TangemBadge(
|
||||||
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
|
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
|
visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End,
|
||||||
modifier = Modifier.size(size = size.toContentSize()),
|
modifier = Modifier.size(size = size.toContentSize()),
|
||||||
label = "Start Icon Visibility",
|
label = "Start Icon Visibility",
|
||||||
) {
|
) {
|
||||||
|
|
@ -105,13 +105,18 @@ fun TangemBadge(
|
||||||
tint = iconColor,
|
tint = iconColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
AnimatedVisibility(
|
||||||
text = text.resolveReference(),
|
visible = text != null,
|
||||||
style = size.toTextStyle(),
|
label = "Text Visibility",
|
||||||
maxLines = 1,
|
) {
|
||||||
color = getTextColor(type = type, color = color),
|
val wrappedText = remember(this) { requireNotNull(text) }
|
||||||
)
|
Text(
|
||||||
|
text = wrappedText.resolveReference(),
|
||||||
|
style = size.toTextStyle(),
|
||||||
|
maxLines = 1,
|
||||||
|
color = getTextColor(type = type, color = color),
|
||||||
|
)
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
|
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
|
||||||
modifier = Modifier.size(size = size.toContentSize()),
|
modifier = Modifier.size(size = size.toContentSize()),
|
||||||
|
|
@ -178,14 +183,17 @@ enum class TangemBadgeSize {
|
||||||
X4 -> when (position) {
|
X4 -> when (position) {
|
||||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
|
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
|
||||||
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
|
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
|
||||||
|
TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp)
|
||||||
}
|
}
|
||||||
X6 -> when (position) {
|
X6 -> when (position) {
|
||||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
|
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
|
||||||
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
|
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
|
||||||
|
TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp)
|
||||||
}
|
}
|
||||||
X9 -> when (position) {
|
X9 -> when (position) {
|
||||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
|
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
|
||||||
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
|
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
|
||||||
|
TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +230,7 @@ enum class TangemBadgeSize {
|
||||||
enum class TangemBadgeIconPosition {
|
enum class TangemBadgeIconPosition {
|
||||||
Start,
|
Start,
|
||||||
End,
|
End,
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -240,6 +249,7 @@ enum class TangemBadgeColor {
|
||||||
Blue,
|
Blue,
|
||||||
Red,
|
Red,
|
||||||
Gray,
|
Gray,
|
||||||
|
Green,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
|
|
@ -258,6 +268,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
||||||
-> TangemTheme.colors2.markers.iconRed
|
-> TangemTheme.colors2.markers.iconRed
|
||||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||||
}
|
}
|
||||||
|
TangemBadgeColor.Green -> when (type) {
|
||||||
|
TangemBadgeType.Outline,
|
||||||
|
TangemBadgeType.Tinted,
|
||||||
|
-> TangemTheme.colors2.markers.iconGreen
|
||||||
|
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
|
|
@ -276,8 +292,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
||||||
-> TangemTheme.colors2.markers.textRed
|
-> TangemTheme.colors2.markers.textRed
|
||||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||||
}
|
}
|
||||||
|
TangemBadgeColor.Green -> when (type) {
|
||||||
|
TangemBadgeType.Outline,
|
||||||
|
TangemBadgeType.Tinted,
|
||||||
|
-> TangemTheme.colors2.markers.textGreen
|
||||||
|
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod")
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@Composable
|
||||||
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
|
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
|
||||||
|
|
@ -286,6 +309,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
||||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
|
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
|
||||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
|
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
|
||||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
|
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
|
||||||
|
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
TangemBadgeType.Tinted -> background(
|
TangemBadgeType.Tinted -> background(
|
||||||
|
|
@ -293,6 +317,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
||||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
|
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
|
||||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
|
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
|
||||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
|
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
|
||||||
|
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
TangemBadgeType.Outline -> {
|
TangemBadgeType.Outline -> {
|
||||||
|
|
@ -301,6 +326,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
||||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
|
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
|
||||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
|
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
|
||||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
|
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
|
||||||
|
TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen
|
||||||
},
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
|
|
@ -320,16 +346,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl
|
||||||
.background(TangemTheme.colors2.surface.level1)
|
.background(TangemTheme.colors2.surface.level1)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
repeat(2) { yIndex ->
|
repeat(3) { yIndex ->
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
repeat(TangemBadgeType.entries.size) { index ->
|
repeat(TangemBadgeType.entries.size) { index ->
|
||||||
TangemBadge(
|
TangemBadge(
|
||||||
text = stringReference("Title"),
|
text = stringReference("Title").takeIf { yIndex < 2 },
|
||||||
iconRes = R.drawable.ic_information_24,
|
iconRes = R.drawable.ic_information_24,
|
||||||
type = TangemBadgeType.entries[index],
|
type = TangemBadgeType.entries[index],
|
||||||
color = params,
|
color = params,
|
||||||
shape = TangemBadgeShape.entries[yIndex % 2],
|
shape = TangemBadgeShape.entries[yIndex % 2],
|
||||||
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
|
iconPosition = TangemBadgeIconPosition.entries[yIndex],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -344,6 +370,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeC
|
||||||
TangemBadgeColor.Gray,
|
TangemBadgeColor.Gray,
|
||||||
TangemBadgeColor.Blue,
|
TangemBadgeColor.Blue,
|
||||||
TangemBadgeColor.Red,
|
TangemBadgeColor.Red,
|
||||||
|
TangemBadgeColor.Green,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.flicker
|
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
|
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
|
||||||
import com.tangem.core.ui.ds.button.*
|
import com.tangem.core.ui.ds.button.*
|
||||||
import com.tangem.core.ui.ds.image.TangemIcon
|
import com.tangem.core.ui.ds.image.TangemIcon
|
||||||
|
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||||
import com.tangem.core.ui.extensions.*
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
|
@ -58,7 +58,15 @@ fun TangemMessage(
|
||||||
if (messageUM.iconUM != null) {
|
if (messageUM.iconUM != null) {
|
||||||
TangemIcon(
|
TangemIcon(
|
||||||
tangemIconUM = messageUM.iconUM,
|
tangemIconUM = messageUM.iconUM,
|
||||||
modifier = Modifier.size(TangemTheme.dimens2.x8),
|
modifier = Modifier
|
||||||
|
.align(
|
||||||
|
if (messageUM.buttonsUM.isEmpty()) {
|
||||||
|
Alignment.CenterVertically
|
||||||
|
} else {
|
||||||
|
Alignment.Top
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size(TangemTheme.dimens2.x7),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider<TangemMess
|
||||||
id = "1",
|
id = "1",
|
||||||
title = stringReference("Title text"),
|
title = stringReference("Title text"),
|
||||||
subtitle = stringReference("Subtext"),
|
subtitle = stringReference("Subtext"),
|
||||||
|
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||||
messageEffect = TangemMessageEffect.None,
|
messageEffect = TangemMessageEffect.None,
|
||||||
isCentered = true,
|
isCentered = true,
|
||||||
),
|
),
|
||||||
|
|
@ -350,6 +359,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider<TangemMess
|
||||||
title = stringReference("Title text"),
|
title = stringReference("Title text"),
|
||||||
subtitle = stringReference("Subtext"),
|
subtitle = stringReference("Subtext"),
|
||||||
messageEffect = TangemMessageEffect.Magic,
|
messageEffect = TangemMessageEffect.Magic,
|
||||||
|
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||||
isCentered = false,
|
isCentered = false,
|
||||||
buttonsUM = persistentListOf(
|
buttonsUM = persistentListOf(
|
||||||
TangemMessageButtonUM(
|
TangemMessageButtonUM(
|
||||||
|
|
@ -405,9 +415,8 @@ private fun TangemMessage2_Preview() {
|
||||||
content = {
|
content = {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(TangemTheme.dimens2.x10)
|
.size(TangemTheme.dimens2.x7)
|
||||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x2))
|
.clip(RoundedCornerShape(TangemTheme.dimens2.x2))
|
||||||
.flicker(isFlickering = true)
|
|
||||||
.background(TangemTheme.colors2.text.neutral.primary),
|
.background(TangemTheme.colors2.text.neutral.primary),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package com.tangem.core.ui.ds.opportunities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.draw.innerShadow
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.shadow.Shadow
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
|
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
|
||||||
|
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||||
|
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import dev.chrisbanes.haze.HazeStyle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container that draws a blurred background (from URL or solid color) and
|
||||||
|
* applies a semi‑transparent overlay on top of it, then renders foreground content.
|
||||||
|
*
|
||||||
|
* Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev
|
||||||
|
*
|
||||||
|
* @param icon Background configuration (URL, solid color or none).
|
||||||
|
* @param modifier Modifier applied to the outer container.
|
||||||
|
* @param content Foreground content rendered on top of the overlay.
|
||||||
|
* @param shape Shape used for inner shadow and border (e.g. rounded corners).
|
||||||
|
*/
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
@Composable
|
||||||
|
fun OpportunitiesBG(
|
||||||
|
icon: TangemIconUM,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = RoundedCornerShape(16.dp),
|
||||||
|
content: @Composable BoxScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val isInDarkTheme = LocalIsInDarkTheme.current
|
||||||
|
val overlayColor = remember(isInDarkTheme) {
|
||||||
|
if (isInDarkTheme) {
|
||||||
|
Color(OVERLAY_DARK)
|
||||||
|
} else {
|
||||||
|
Color.White
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
BackgroundLayer(icon = icon)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(shape)
|
||||||
|
.innerShadow(
|
||||||
|
shape = shape,
|
||||||
|
shadow = Shadow(
|
||||||
|
radius = 30.dp,
|
||||||
|
spread = 5.dp,
|
||||||
|
color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f),
|
||||||
|
offset = DpOffset(0.dp, 0.dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerShadow(
|
||||||
|
shape = shape,
|
||||||
|
shadow = Shadow(
|
||||||
|
radius = 100.dp,
|
||||||
|
spread = (-39).dp,
|
||||||
|
color = Color(INNER_SHADOW_COLOR_END).copy(.3f),
|
||||||
|
offset = DpOffset(0.dp, (-56).dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerShadow(
|
||||||
|
shape = shape,
|
||||||
|
shadow = Shadow(
|
||||||
|
radius = 40.dp,
|
||||||
|
spread = (-19).dp,
|
||||||
|
color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f),
|
||||||
|
offset = DpOffset(0.dp, (-16).dp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.drawWithContent {
|
||||||
|
drawRect(color = overlayColor.copy(alpha = .7f))
|
||||||
|
drawContent()
|
||||||
|
val outline = shape.createOutline(size, layoutDirection, this)
|
||||||
|
drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx()))
|
||||||
|
},
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod")
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) {
|
||||||
|
when (icon) {
|
||||||
|
is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius)
|
||||||
|
is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius)
|
||||||
|
is TangemIconUM.Ident -> Unit
|
||||||
|
is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod")
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) {
|
||||||
|
when (state) {
|
||||||
|
is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground(
|
||||||
|
color = state.color,
|
||||||
|
blurRadius = blurRadius,
|
||||||
|
)
|
||||||
|
is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground(
|
||||||
|
color = state.color,
|
||||||
|
blurRadius = blurRadius,
|
||||||
|
)
|
||||||
|
is CurrencyIconState.CustomTokenIcon -> SolidColorBackground(
|
||||||
|
color = state.background,
|
||||||
|
blurRadius = blurRadius,
|
||||||
|
)
|
||||||
|
is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius)
|
||||||
|
is CurrencyIconState.CoinIcon -> {
|
||||||
|
state.url?.let {
|
||||||
|
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||||
|
} ?: run {
|
||||||
|
ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CurrencyIconState.FiatIcon -> state.url?.let {
|
||||||
|
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||||
|
} ?: run {
|
||||||
|
ResBackground(res = state.fallbackResId, blurRadius = blurRadius)
|
||||||
|
}
|
||||||
|
is CurrencyIconState.TokenIcon -> state.url?.let {
|
||||||
|
UrlBackground(imageUrl = state.url, blurRadius = blurRadius)
|
||||||
|
} ?: run {
|
||||||
|
SolidColorBackground(
|
||||||
|
color = state.fallbackBackground,
|
||||||
|
blurRadius = blurRadius,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CurrencyIconState.Loading -> Unit
|
||||||
|
CurrencyIconState.Locked -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val imageRequest = remember(imageUrl) {
|
||||||
|
if (imageUrl.isNullOrBlank()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.crossfade(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageRequest != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageRequest,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.scale(SCALE_FACTOR)
|
||||||
|
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(res),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.scale(SCALE_FACTOR)
|
||||||
|
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(color = color)
|
||||||
|
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SCALE_FACTOR = 1.5f
|
||||||
|
private const val INNER_SHADOW_COLOR_START = 0x00000000
|
||||||
|
private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF
|
||||||
|
|
||||||
|
private const val BORDER_COLOR = 0xFFF0F0F0
|
||||||
|
private const val OVERLAY_DARK = 0xFF141414
|
||||||
|
|
||||||
|
// region Previews
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Composable
|
||||||
|
private fun OpportunitiesBGPreview() {
|
||||||
|
TangemThemePreview {
|
||||||
|
OpportunitiesBG(
|
||||||
|
modifier = Modifier.size(400.dp),
|
||||||
|
icon = TangemIconUM.Currency(
|
||||||
|
CurrencyIconState.CoinIcon(
|
||||||
|
url = null,
|
||||||
|
fallbackResId = R.drawable.img_solana_22,
|
||||||
|
isGrayscale = false,
|
||||||
|
shouldShowCustomBadge = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
@ -16,8 +16,8 @@ import kotlin.math.max
|
||||||
/**
|
/**
|
||||||
* A custom layout composable that arranges its children in a row with specific layout IDs.
|
* A custom layout composable that arranges its children in a row with specific layout IDs.
|
||||||
*/
|
*/
|
||||||
internal enum class TangemRowLayoutId {
|
enum class TangemRowLayoutId {
|
||||||
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
|
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId {
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TangemRowContainer(
|
fun TangemRowContainer(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
|
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
|
|
@ -37,6 +37,7 @@ internal fun TangemRowContainer(
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val localDirection = LocalLayoutDirection.current
|
val localDirection = LocalLayoutDirection.current
|
||||||
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
|
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
|
||||||
|
val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() }
|
||||||
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
|
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
|
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
|
||||||
|
|
@ -110,6 +111,10 @@ internal fun TangemRowContainer(
|
||||||
layoutId = TangemRowLayoutId.EXTRA_TOP,
|
layoutId = TangemRowLayoutId.EXTRA_TOP,
|
||||||
constraints = constraints,
|
constraints = constraints,
|
||||||
)
|
)
|
||||||
|
val extraBottomPlaceable = measurables.measure(
|
||||||
|
layoutId = TangemRowLayoutId.EXTRA_BOTTOM,
|
||||||
|
constraints = constraints,
|
||||||
|
)
|
||||||
|
|
||||||
val mainLayoutHeight = maxOf(
|
val mainLayoutHeight = maxOf(
|
||||||
headPlaceable.heightOrZero(),
|
headPlaceable.heightOrZero(),
|
||||||
|
|
@ -124,7 +129,13 @@ internal fun TangemRowContainer(
|
||||||
contentTopPadding
|
contentTopPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
|
val mainContentBottomPadding = if (extraBottomPlaceable != null) {
|
||||||
|
extraBottomPlaceable.heightOrZero() + contentBottomPadding
|
||||||
|
} else {
|
||||||
|
contentBottomPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding
|
||||||
|
|
||||||
layout(width = constraints.maxWidth, height = layoutHeight) {
|
layout(width = constraints.maxWidth, height = layoutHeight) {
|
||||||
extraTopPlaceable?.placeRelative(x = 0, y = 0)
|
extraTopPlaceable?.placeRelative(x = 0, y = 0)
|
||||||
|
|
@ -174,6 +185,11 @@ internal fun TangemRowContainer(
|
||||||
x = layoutWidth - tailPlaceable.width + contentEndPadding,
|
x = layoutWidth - tailPlaceable.width + contentEndPadding,
|
||||||
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
|
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extraBottomPlaceable?.placeRelative(
|
||||||
|
x = 0,
|
||||||
|
y = mainContentTopPadding + mainLayoutHeight + extraContentPadding,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,6 @@ fun TangemTokenRow(
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
|
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
|
||||||
)
|
)
|
||||||
|
|
||||||
TokenRowPromoBanner(
|
|
||||||
promoBannerUM = tokenRowUM.promoBannerUM,
|
|
||||||
modifier = Modifier
|
|
||||||
.layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
|
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
|
|
||||||
.padding(horizontal = TangemTheme.dimens2.x3)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
TokenRowTitle(
|
TokenRowTitle(
|
||||||
titleUM = tokenRowUM.titleUM,
|
titleUM = tokenRowUM.titleUM,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -77,17 +68,21 @@ fun TangemTokenRow(
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
||||||
)
|
)
|
||||||
|
|
||||||
TokenRowEndTopContent(
|
TokenRowEndContent(
|
||||||
endContentUM = tokenRowUM.topEndContentUM,
|
endContentUM = tokenRowUM.topEndContentUM,
|
||||||
isBalanceHidden = isBalanceHidden,
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
textStyle = TangemTheme.typography2.bodySemibold16,
|
||||||
|
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
||||||
)
|
)
|
||||||
|
|
||||||
TokenRowEndBottomContent(
|
TokenRowEndContent(
|
||||||
endContentUM = tokenRowUM.bottomEndContentUM,
|
endContentUM = tokenRowUM.bottomEndContentUM,
|
||||||
isBalanceHidden = isBalanceHidden,
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||||
|
textColor = TangemTheme.colors2.text.neutral.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
||||||
|
|
@ -100,6 +95,15 @@ fun TangemTokenRow(
|
||||||
.layoutId(layoutId = TangemRowLayoutId.TAIL)
|
.layoutId(layoutId = TangemRowLayoutId.TAIL)
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
|
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TokenRowPromoBanner(
|
||||||
|
promoBannerUM = tokenRowUM.promoBannerUM,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM)
|
||||||
|
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
|
||||||
|
.padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
|
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
|
||||||
)
|
)
|
||||||
|
|
@ -159,17 +163,21 @@ fun TangemTokenRow(
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
|
||||||
)
|
)
|
||||||
|
|
||||||
TokenRowEndTopContent(
|
TokenRowEndContent(
|
||||||
endContentUM = tokenRowUM.topEndContentUM,
|
endContentUM = tokenRowUM.topEndContentUM,
|
||||||
isBalanceHidden = isBalanceHidden,
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
textStyle = TangemTheme.typography2.bodySemibold16,
|
||||||
|
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
|
||||||
)
|
)
|
||||||
|
|
||||||
TokenRowEndBottomContent(
|
TokenRowEndContent(
|
||||||
endContentUM = tokenRowUM.bottomEndContentUM,
|
endContentUM = tokenRowUM.bottomEndContentUM,
|
||||||
isBalanceHidden = isBalanceHidden,
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||||
|
textColor = TangemTheme.colors2.text.neutral.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
|
||||||
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
||||||
val text: TextReference,
|
val text: TextReference,
|
||||||
val isAvailable: Boolean = true,
|
val isAvailable: Boolean = true,
|
||||||
val isFlickering: Boolean = false,
|
val isFlickering: Boolean = false,
|
||||||
val icons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
val startIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
||||||
|
val endIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
|
||||||
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
|
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
|
||||||
) : EndContentUM()
|
) : EndContentUM()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ internal object TangemTokenRowPreviewData {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
icons = persistentListOf(
|
startIcons = persistentListOf(
|
||||||
TangemIconUM.Icon(R.drawable.ic_staking_mini_10),
|
TangemIconUM.Icon(R.drawable.ic_staking_mini_10),
|
||||||
TangemIconUM.Icon(R.drawable.ic_attention_12),
|
TangemIconUM.Icon(R.drawable.ic_attention_12),
|
||||||
TangemIconUM.Icon(R.drawable.ic_error_sync_24),
|
TangemIconUM.Icon(R.drawable.ic_error_sync_24),
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
package com.tangem.core.ui.ds.row.token.internal
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import com.tangem.core.ui.components.TextShimmer
|
|
||||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
|
||||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
|
||||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
|
||||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
|
||||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun TokenRowEndBottomContent(
|
|
||||||
endContentUM: TangemTokenRowUM.EndContentUM,
|
|
||||||
isBalanceHidden: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
when (endContentUM) {
|
|
||||||
is TangemTokenRowUM.EndContentUM.Content -> Content(
|
|
||||||
modifier = modifier,
|
|
||||||
endContentUM = endContentUM,
|
|
||||||
isBalanceHidden = isBalanceHidden,
|
|
||||||
)
|
|
||||||
TangemTokenRowUM.EndContentUM.Empty -> Unit
|
|
||||||
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
|
|
||||||
style = TangemTheme.typography2.captionSemibold12,
|
|
||||||
modifier = modifier.width(TangemTheme.dimens2.x10),
|
|
||||||
radius = TangemTheme.dimens2.x25,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Content(
|
|
||||||
endContentUM: TangemTokenRowUM.EndContentUM.Content,
|
|
||||||
isBalanceHidden: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
|
|
||||||
isEnabled = endContentUM.isFlickering,
|
|
||||||
textColor = if (endContentUM.isAvailable) {
|
|
||||||
TangemTheme.colors2.text.neutral.secondary
|
|
||||||
} else {
|
|
||||||
TangemTheme.colors2.text.status.disabled
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
when (val priceChangeUM = endContentUM.priceChangeUM) {
|
|
||||||
is PriceChangeState.Content -> TokenRowPriceChangeContent(
|
|
||||||
priceChangeState = priceChangeUM,
|
|
||||||
isFlickering = endContentUM.isFlickering,
|
|
||||||
isAvailable = endContentUM.isAvailable,
|
|
||||||
)
|
|
||||||
PriceChangeState.Unknown -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Preview
|
|
||||||
@Composable
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
private fun TokenRowEndBottomContent_Preview(
|
|
||||||
@PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
|
|
||||||
) {
|
|
||||||
TangemThemePreviewRedesign {
|
|
||||||
TokenRowEndBottomContent(
|
|
||||||
endContentUM = params,
|
|
||||||
isBalanceHidden = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
|
|
||||||
override val values: Sequence<TangemTokenRowUM.EndContentUM>
|
|
||||||
get() = sequenceOf(
|
|
||||||
TangemTokenRowPreviewData.bottomEndContentUM,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
@ -8,15 +8,18 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import com.tangem.core.ui.components.TextShimmer
|
import com.tangem.core.ui.components.TextShimmer
|
||||||
|
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||||
|
|
@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TokenRowEndTopContent(
|
internal fun TokenRowEndContent(
|
||||||
endContentUM: TangemTokenRowUM.EndContentUM,
|
endContentUM: TangemTokenRowUM.EndContentUM,
|
||||||
isBalanceHidden: Boolean,
|
isBalanceHidden: Boolean,
|
||||||
|
textStyle: TextStyle,
|
||||||
|
textColor: Color,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (endContentUM) {
|
when (endContentUM) {
|
||||||
|
|
@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
endContentUM = endContentUM,
|
endContentUM = endContentUM,
|
||||||
isBalanceHidden = isBalanceHidden,
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
textStyle = textStyle,
|
||||||
|
textColor = textColor,
|
||||||
)
|
)
|
||||||
TangemTokenRowUM.EndContentUM.Empty -> Unit
|
TangemTokenRowUM.EndContentUM.Empty -> Unit
|
||||||
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
|
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
|
||||||
style = TangemTheme.typography2.bodySemibold16,
|
style = textStyle,
|
||||||
modifier = modifier.width(TangemTheme.dimens2.x18),
|
modifier = modifier.width(TangemTheme.dimens2.x10),
|
||||||
radius = TangemTheme.dimens2.x25,
|
radius = TangemTheme.dimens2.x25,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent(
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
endContentUM: TangemTokenRowUM.EndContentUM.Content,
|
endContentUM: TangemTokenRowUM.EndContentUM.Content,
|
||||||
|
textStyle: TextStyle,
|
||||||
|
textColor: Color,
|
||||||
isBalanceHidden: Boolean,
|
isBalanceHidden: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -56,14 +65,14 @@ private fun Content(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = endContentUM.icons.isNotEmpty(),
|
visible = endContentUM.startIcons.isNotEmpty(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
|
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||||
) {
|
) {
|
||||||
endContentUM.icons.fastForEach { icon ->
|
endContentUM.startIcons.fastForEach { icon ->
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
||||||
|
|
@ -75,11 +84,11 @@ private fun Content(
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier,
|
|
||||||
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
|
color = textColor,
|
||||||
|
style = textStyle.applyBladeBrush(
|
||||||
isEnabled = endContentUM.isFlickering,
|
isEnabled = endContentUM.isFlickering,
|
||||||
textColor = if (endContentUM.isAvailable) {
|
textColor = if (endContentUM.isAvailable) {
|
||||||
TangemTheme.colors2.text.neutral.primary
|
TangemTheme.colors2.text.neutral.primary
|
||||||
|
|
@ -88,6 +97,34 @@ private fun Content(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = endContentUM.endIcons.isNotEmpty(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||||
|
) {
|
||||||
|
endContentUM.endIcons.fastForEach { icon ->
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(TangemTheme.dimens2.x3),
|
||||||
|
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
||||||
|
tint = icon.tintReference(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val priceChangeUM = endContentUM.priceChangeUM) {
|
||||||
|
is PriceChangeState.Content -> TokenRowPriceChangeContent(
|
||||||
|
priceChangeState = priceChangeUM,
|
||||||
|
isFlickering = endContentUM.isFlickering,
|
||||||
|
isAvailable = endContentUM.isAvailable,
|
||||||
|
)
|
||||||
|
PriceChangeState.Unknown -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,13 +132,15 @@ private fun Content(
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
private fun TokenRowEndTopContent_Preview(
|
private fun TokenRowEndContent_Preview(
|
||||||
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
|
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
|
||||||
) {
|
) {
|
||||||
TangemThemePreviewRedesign {
|
TangemThemePreviewRedesign {
|
||||||
TokenRowEndTopContent(
|
TokenRowEndContent(
|
||||||
endContentUM = params,
|
endContentUM = params,
|
||||||
isBalanceHidden = false,
|
isBalanceHidden = false,
|
||||||
|
textColor = TangemTheme.colors2.text.neutral.primary,
|
||||||
|
textStyle = TangemTheme.typography2.captionSemibold12,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview(
|
||||||
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
|
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
|
||||||
override val values: Sequence<TangemTokenRowUM.EndContentUM>
|
override val values: Sequence<TangemTokenRowUM.EndContentUM>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
TangemTokenRowPreviewData.topEndContentUM,
|
TangemTokenRowPreviewData.bottomEndContentUM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
@ -19,6 +16,7 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.ds.badge.*
|
||||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
|
@ -40,55 +38,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C
|
||||||
LaunchedEffect(promoBannerUM) {
|
LaunchedEffect(promoBannerUM) {
|
||||||
promoBannerUM.onPromoShown()
|
promoBannerUM.onPromoShown()
|
||||||
}
|
}
|
||||||
val bgColor = TangemTheme.colors.control.default
|
val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen
|
||||||
Column(modifier = modifier) {
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.shape_triangular),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = bgColor,
|
||||||
|
modifier = Modifier.padding(start = TangemTheme.dimens2.x5),
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
|
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
|
||||||
.clickable(onClick = promoBannerUM.onPromoBannerClick)
|
.clickable(onClick = promoBannerUM.onPromoBannerClick)
|
||||||
.padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
|
.padding(
|
||||||
.fillMaxWidth(),
|
start = TangemTheme.dimens2.x2_5,
|
||||||
|
end = TangemTheme.dimens2.x0_5,
|
||||||
|
top = TangemTheme.dimens2.x0_5,
|
||||||
|
bottom = TangemTheme.dimens2.x0_5,
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
|
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = TangemTheme.colors.icon.accent,
|
tint = TangemTheme.colors2.markers.textGreen,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = TangemTheme.dimens2.x2)
|
.padding(vertical = TangemTheme.dimens2.x0_5)
|
||||||
.size(TangemTheme.dimens2.x4),
|
.size(TangemTheme.dimens2.x3),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = promoBannerUM.title.resolveReference(),
|
text = promoBannerUM.title.resolveReference(),
|
||||||
style = TangemTheme.typography2.captionSemibold12,
|
style = TangemTheme.typography2.captionSemibold11,
|
||||||
color = TangemTheme.colors2.text.neutral.secondary,
|
color = TangemTheme.colors2.markers.textGreen,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.padding(vertical = TangemTheme.dimens2.x0_5),
|
||||||
.padding(end = TangemTheme.dimens2.x2),
|
|
||||||
)
|
)
|
||||||
Icon(
|
TangemBadge(
|
||||||
painter = painterResource(id = R.drawable.ic_close_24),
|
size = TangemBadgeSize.X4,
|
||||||
contentDescription = null,
|
shape = TangemBadgeShape.Rounded,
|
||||||
tint = TangemTheme.colors2.text.neutral.secondary,
|
color = TangemBadgeColor.Green,
|
||||||
modifier = Modifier
|
type = TangemBadgeType.Tinted,
|
||||||
.size(TangemTheme.dimens2.x4)
|
iconRes = R.drawable.ic_close_24,
|
||||||
.clickable(
|
iconPosition = TangemBadgeIconPosition.None,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
onClick = promoBannerUM.onCloseClick,
|
||||||
indication = ripple(bounded = false),
|
|
||||||
onClick = { promoBannerUM.onCloseClick() },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = bgColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.core.ui.format.bigdecimal
|
package com.tangem.core.ui.format.bigdecimal
|
||||||
|
|
||||||
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
|
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
|
||||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||||
|
|
@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
||||||
import com.tangem.utils.extensions.isNotWhitespace
|
import com.tangem.utils.extensions.isNotWhitespace
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
import java.text.DecimalFormat
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull(
|
||||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class BigDecimalCryptoFormatStyled(
|
||||||
|
val symbol: String,
|
||||||
|
val decimals: Int,
|
||||||
|
val spanStyleReference: SpanStyleReference,
|
||||||
|
val locale: Locale = Locale.getDefault(),
|
||||||
|
val shouldIgnoreSymbolPosition: Boolean = false,
|
||||||
|
) : BigDecimalFormatStyled {
|
||||||
|
|
||||||
|
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
|
||||||
|
}
|
||||||
|
|
||||||
// == Initializers ==
|
// == Initializers ==
|
||||||
|
|
||||||
fun BigDecimalFormatScope.crypto(
|
fun BigDecimalFormatScope.crypto(
|
||||||
|
|
@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BigDecimalFormatScope.cryptoStyled(
|
||||||
|
symbol: String,
|
||||||
|
decimals: Int,
|
||||||
|
spanStyleReference: SpanStyleReference,
|
||||||
|
locale: Locale = Locale.getDefault(),
|
||||||
|
): BigDecimalCryptoFormatStyled {
|
||||||
|
return BigDecimalCryptoFormatStyled(
|
||||||
|
symbol = symbol,
|
||||||
|
decimals = decimals,
|
||||||
|
spanStyleReference = spanStyleReference,
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimalFormatScope.cryptoStyled(
|
||||||
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
spanStyleReference: SpanStyleReference,
|
||||||
|
ignoreSymbolPosition: Boolean = false,
|
||||||
|
locale: Locale = Locale.getDefault(),
|
||||||
|
): BigDecimalCryptoFormatStyled {
|
||||||
|
return BigDecimalCryptoFormatStyled(
|
||||||
|
symbol = cryptoCurrency.symbol,
|
||||||
|
decimals = cryptoCurrency.decimals,
|
||||||
|
spanStyleReference = spanStyleReference,
|
||||||
|
shouldIgnoreSymbolPosition = ignoreSymbolPosition,
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// == Formatters ==
|
// == Formatters ==
|
||||||
|
|
||||||
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
||||||
|
|
@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) =
|
||||||
|
BigDecimalFormatStyled { value ->
|
||||||
|
if (shouldIgnoreSymbolPosition) {
|
||||||
|
val formatter = NumberFormat.getInstance(locale).apply {
|
||||||
|
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||||
|
minimumFractionDigits = 2
|
||||||
|
isGroupingUsed = true
|
||||||
|
roundingMode = RoundingMode.HALF_UP
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedAmount = formatter.format(value)
|
||||||
|
|
||||||
|
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||||
|
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||||
|
|
||||||
|
combinedReference(
|
||||||
|
stringReference(formattedAmount.take(separatorIndex)),
|
||||||
|
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
|
||||||
|
stringReference(NON_BREAKING_SPACE + symbol),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||||
|
currency = usdCurrency
|
||||||
|
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||||
|
minimumFractionDigits = 2
|
||||||
|
isGroupingUsed = true
|
||||||
|
roundingMode = RoundingMode.HALF_UP
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedAmount = formatter.format(value)
|
||||||
|
.replaceFiatSymbolWithCrypto(
|
||||||
|
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||||
|
cryptoCurrencySymbol = symbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||||
|
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||||
|
|
||||||
|
combinedReference(
|
||||||
|
stringReference(formattedAmount.take(separatorIndex)),
|
||||||
|
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
|
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
|
||||||
val formatter = if (value.isMoreThanThreshold()) {
|
val formatter = if (value.isMoreThanThreshold()) {
|
||||||
NumberFormat.getCurrencyInstance(locale).apply {
|
NumberFormat.getCurrencyInstance(locale).apply {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.tangem.core.ui.format.bigdecimal
|
package com.tangem.core.ui.format.bigdecimal
|
||||||
|
|
||||||
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
import java.text.DecimalFormat
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
@ -15,8 +17,16 @@ open class BigDecimalFiatFormat(
|
||||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Initializers ==
|
open class BigDecimalFiatFormatStyled(
|
||||||
|
val fiatCurrencyCode: String,
|
||||||
|
val fiatCurrencySymbol: String,
|
||||||
|
val spanStyleReference: SpanStyleReference,
|
||||||
|
val locale: Locale = Locale.getDefault(),
|
||||||
|
) : BigDecimalFormatStyled {
|
||||||
|
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
//region == Initializers ==
|
||||||
fun BigDecimalFormatScope.fiat(
|
fun BigDecimalFormatScope.fiat(
|
||||||
fiatCurrencyCode: String,
|
fiatCurrencyCode: String,
|
||||||
fiatCurrencySymbol: String,
|
fiatCurrencySymbol: String,
|
||||||
|
|
@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Formatters ==
|
fun BigDecimalFormatScope.fiat(
|
||||||
|
fiatCurrencyCode: String,
|
||||||
|
fiatCurrencySymbol: String,
|
||||||
|
spanStyleReference: SpanStyleReference,
|
||||||
|
locale: Locale = Locale.getDefault(),
|
||||||
|
): BigDecimalFiatFormatStyled {
|
||||||
|
return BigDecimalFiatFormatStyled(
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||||
|
spanStyleReference = spanStyleReference,
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// endregion == Formatters ==
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats fiat amount with default precision.
|
* Formats fiat amount with default precision.
|
||||||
|
|
@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value ->
|
||||||
|
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||||
|
|
||||||
|
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||||
|
currency = formatterCurrency
|
||||||
|
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||||
|
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||||
|
roundingMode = RoundingMode.HALF_UP
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattingAmount = if (value.isLessThanThreshold()) {
|
||||||
|
FIAT_FORMAT_THRESHOLD
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||||
|
val formattedAmount = formatter.format(formattingAmount)
|
||||||
|
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||||
|
|
||||||
|
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||||
|
|
||||||
|
val wholePart = formattedAmount.take(separatorIndex)
|
||||||
|
val fractionalPart = formattedAmount.drop(separatorIndex)
|
||||||
|
|
||||||
|
combinedReference(
|
||||||
|
if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY,
|
||||||
|
stringReference(wholePart),
|
||||||
|
styledStringReference(fractionalPart, spanStyleReference),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats fiat amount with default precision and adds tilde sign
|
* Formats fiat amount with default precision and adds tilde sign
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
package com.tangem.core.ui.format.bigdecimal
|
package com.tangem.core.ui.format.bigdecimal
|
||||||
|
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
interface BigDecimalFormatScope {
|
interface BigDecimalFormatScope {
|
||||||
companion object { val Empty = object : BigDecimalFormatScope {} }
|
companion object {
|
||||||
|
val Empty = object : BigDecimalFormatScope {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
|
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
|
||||||
|
|
||||||
|
fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope
|
||||||
|
|
||||||
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
|
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
|
||||||
return BigDecimalFormatScope.Empty.block()(this)
|
return BigDecimalFormatScope.Empty.block()(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference {
|
||||||
|
return BigDecimalFormatScope.Empty.block()(this)
|
||||||
|
}
|
||||||
|
|
||||||
inline fun BigDecimal?.format(
|
inline fun BigDecimal?.format(
|
||||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||||
block: BigDecimalFormatScope.() -> BigDecimalFormat,
|
block: BigDecimalFormatScope.() -> BigDecimalFormat,
|
||||||
|
|
@ -20,6 +30,14 @@ inline fun BigDecimal?.format(
|
||||||
return BigDecimalFormatScope.Empty.block()(this)
|
return BigDecimalFormatScope.Empty.block()(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun BigDecimal?.formatStyled(
|
||||||
|
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||||
|
block: BigDecimalFormatScope.() -> BigDecimalFormatStyled,
|
||||||
|
): TextReference {
|
||||||
|
if (this == null) return stringReference(fallbackString)
|
||||||
|
return BigDecimalFormatScope.Empty.block()(this)
|
||||||
|
}
|
||||||
|
|
||||||
fun BigDecimal?.format(
|
fun BigDecimal?.format(
|
||||||
format: BigDecimalFormat,
|
format: BigDecimalFormat,
|
||||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||||
|
|
@ -27,3 +45,11 @@ fun BigDecimal?.format(
|
||||||
if (this == null) return fallbackString
|
if (this == null) return fallbackString
|
||||||
return format(this)
|
return format(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BigDecimal?.format(
|
||||||
|
format: BigDecimalFormatStyled,
|
||||||
|
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||||
|
): TextReference {
|
||||||
|
if (this == null) return stringReference(fallbackString)
|
||||||
|
return format(this)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ object TangemColorPalette {
|
||||||
val Dark4 = Color(0xFF3B3B3B)
|
val Dark4 = Color(0xFF3B3B3B)
|
||||||
val Dark5 = Color(0xFF303030)
|
val Dark5 = Color(0xFF303030)
|
||||||
val Dark6 = Color(0xFF1E1E1E)
|
val Dark6 = Color(0xFF1E1E1E)
|
||||||
|
val Dark7 = Color(0xFF171717)
|
||||||
// endregion Dark
|
// endregion Dark
|
||||||
|
|
||||||
// region Dark Alpha
|
// region Dark Alpha
|
||||||
|
|
@ -58,20 +59,45 @@ object TangemColorPalette {
|
||||||
val DarkGreen = Color(0xFF06311F)
|
val DarkGreen = Color(0xFF06311F)
|
||||||
// endregion Green
|
// endregion Green
|
||||||
|
|
||||||
// region Blue
|
// region Azure
|
||||||
val Azure = Color(0xFF0099FF)
|
val Azure = Color(0xFF0099FF)
|
||||||
// endregion Blue
|
val Azure_50 = Color(0x800099FF)
|
||||||
|
val Azure_10 = Color(0x1A0099FF)
|
||||||
|
// endregion Azure
|
||||||
|
|
||||||
// region Red
|
// region Amaranth
|
||||||
val Amaranth = Color(0xFFFF3333)
|
val Amaranth = Color(0xFFFF3333)
|
||||||
|
val Amaranth_50 = Color(0x80FF3333)
|
||||||
|
val Amaranth_20 = Color(0x33FF3333)
|
||||||
|
val Amaranth_10 = Color(0x1AFF3333)
|
||||||
|
// endregion Amaranth
|
||||||
|
|
||||||
|
// region Flamingo
|
||||||
val Flamingo = Color(0xFFFF5B5B)
|
val Flamingo = Color(0xFFFF5B5B)
|
||||||
// endregion Red
|
val Flamingo_50 = Color(0x80FF5B5B)
|
||||||
|
val Flamingo_20 = Color(0x33FF5B5B)
|
||||||
|
val Flamingo_10 = Color(0x1AFF5B5B)
|
||||||
|
// endregion Flamingo
|
||||||
|
|
||||||
// region Yellow
|
// region Yellow
|
||||||
val Tangerine = Color(0xFFFFB71B)
|
val Tangerine = Color(0xFFFFB71B)
|
||||||
val Mustard = Color(0xFFFDDE55)
|
val Mustard = Color(0xFFFDDE55)
|
||||||
// endregion Yellow
|
// endregion Yellow
|
||||||
|
|
||||||
|
// region Emerald
|
||||||
|
val Emerald = Color(0xFF34DF12)
|
||||||
|
val Emerald_50 = Color(0x8034DF12)
|
||||||
|
val Emerald_20 = Color(0x3334DF12)
|
||||||
|
val Emerald_10 = Color(0x1A34DF12)
|
||||||
|
// endregion Emerald
|
||||||
|
|
||||||
|
// region Eucalyptus
|
||||||
|
val Eucalyptus = Color(0xFF0C9F3D)
|
||||||
|
val Eucalyptus_50 = Color(0x800C9F3D)
|
||||||
|
val Eucalyptus_20 = Color(0x330C9F3D)
|
||||||
|
val Eucalyptus_10 = Color(0x1A0C9F3D)
|
||||||
|
// endregion Eucalyptus
|
||||||
|
|
||||||
// region Overlay
|
// region Overlay
|
||||||
val Overlay1 = Color(0x66000000)
|
val Overlay1 = Color(0x66000000)
|
||||||
val Overlay2 = Color(0xB2000000)
|
val Overlay2 = Color(0xB2000000)
|
||||||
|
|
|
||||||
|
|
@ -448,24 +448,36 @@ class TangemColors2 internal constructor(
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class Markers internal constructor(
|
class Markers internal constructor(
|
||||||
backgroundSolidGray: Color,
|
|
||||||
backgroundDisabled: Color,
|
|
||||||
backgroundSolidBlue: Color,
|
|
||||||
textGray: Color,
|
|
||||||
textDisabled: Color,
|
textDisabled: Color,
|
||||||
iconGray: Color,
|
|
||||||
iconDisabled: Color,
|
iconDisabled: Color,
|
||||||
|
backgroundDisabled: Color,
|
||||||
|
textGray: Color,
|
||||||
|
iconGray: Color,
|
||||||
borderGray: Color,
|
borderGray: Color,
|
||||||
backgroundTintedBlue: Color,
|
backgroundSolidGray: Color,
|
||||||
|
backgroundTintedGray: Color,
|
||||||
textBlue: Color,
|
textBlue: Color,
|
||||||
|
iconBlue: Color,
|
||||||
|
borderTintedBlue: Color,
|
||||||
|
backgroundSolidBlue: Color,
|
||||||
|
backgroundTintedBlue: Color,
|
||||||
|
textRed: Color,
|
||||||
|
iconRed: Color,
|
||||||
|
borderTintedRed: Color,
|
||||||
backgroundSolidRed: Color,
|
backgroundSolidRed: Color,
|
||||||
backgroundTintedRed: Color,
|
backgroundTintedRed: Color,
|
||||||
iconBlue: Color,
|
textGreen: Color,
|
||||||
iconRed: Color,
|
iconGreen: Color,
|
||||||
textRed: Color,
|
borderTintedGreen: Color,
|
||||||
backgroundTintedGray: Color,
|
borderSolidColor: Color,
|
||||||
borderTintedBlue: Color,
|
backgroundTintedGreen: Color,
|
||||||
borderTintedRed: Color,
|
backgroundSolidGreen: Color,
|
||||||
|
textGreenAlt: Color,
|
||||||
|
iconGreenAlt: Color,
|
||||||
|
borderTintedGreenAlt: Color,
|
||||||
|
borderSolidColorAlt: Color,
|
||||||
|
backgroundTintedGreenAlt: Color,
|
||||||
|
backgroundSolidGreenAlt: Color,
|
||||||
) {
|
) {
|
||||||
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
|
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
|
||||||
private set
|
private set
|
||||||
|
|
@ -504,6 +516,32 @@ class TangemColors2 internal constructor(
|
||||||
var borderTintedRed by mutableStateOf(borderTintedRed)
|
var borderTintedRed by mutableStateOf(borderTintedRed)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var textGreen by mutableStateOf(textGreen)
|
||||||
|
private set
|
||||||
|
var iconGreen by mutableStateOf(iconGreen)
|
||||||
|
private set
|
||||||
|
var borderTintedGreen by mutableStateOf(borderTintedGreen)
|
||||||
|
private set
|
||||||
|
var borderSolidColor by mutableStateOf(borderSolidColor)
|
||||||
|
private set
|
||||||
|
var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen)
|
||||||
|
private set
|
||||||
|
var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen)
|
||||||
|
private set
|
||||||
|
var textGreenAlt by mutableStateOf(textGreenAlt)
|
||||||
|
private set
|
||||||
|
var iconGreenAlt by mutableStateOf(iconGreenAlt)
|
||||||
|
private set
|
||||||
|
var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt)
|
||||||
|
private set
|
||||||
|
var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt)
|
||||||
|
private set
|
||||||
|
var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt)
|
||||||
|
private set
|
||||||
|
|
||||||
fun update(other: Markers) {
|
fun update(other: Markers) {
|
||||||
backgroundSolidGray = other.backgroundSolidGray
|
backgroundSolidGray = other.backgroundSolidGray
|
||||||
backgroundDisabled = other.backgroundDisabled
|
backgroundDisabled = other.backgroundDisabled
|
||||||
|
|
@ -523,6 +561,18 @@ class TangemColors2 internal constructor(
|
||||||
backgroundTintedGray = other.backgroundTintedGray
|
backgroundTintedGray = other.backgroundTintedGray
|
||||||
borderTintedBlue = other.borderTintedBlue
|
borderTintedBlue = other.borderTintedBlue
|
||||||
borderTintedRed = other.borderTintedRed
|
borderTintedRed = other.borderTintedRed
|
||||||
|
textGreen = other.textGreen
|
||||||
|
iconGreen = other.iconGreen
|
||||||
|
borderTintedGreen = other.borderTintedGreen
|
||||||
|
borderSolidColor = other.borderSolidColor
|
||||||
|
backgroundTintedGreen = other.backgroundTintedGreen
|
||||||
|
backgroundSolidGreen = other.backgroundSolidGreen
|
||||||
|
textGreenAlt = other.textGreenAlt
|
||||||
|
iconGreenAlt = other.iconGreenAlt
|
||||||
|
borderTintedGreenAlt = other.borderTintedGreenAlt
|
||||||
|
borderSolidColorAlt = other.borderSolidColorAlt
|
||||||
|
backgroundTintedGreenAlt = other.backgroundTintedGreenAlt
|
||||||
|
backgroundSolidGreenAlt = other.backgroundSolidGreenAlt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,8 @@ private fun lightThemeColors2(): TangemColors2 {
|
||||||
val surface = TangemColors2.Surface(
|
val surface = TangemColors2.Surface(
|
||||||
level1 = TangemColorPalette.White,
|
level1 = TangemColorPalette.White,
|
||||||
level2 = TangemColorPalette.Light1V2,
|
level2 = TangemColorPalette.Light1V2,
|
||||||
level3 = TangemColorPalette.Light1V2,
|
level3 = TangemColorPalette.White,
|
||||||
level4 = TangemColorPalette.White,
|
level4 = TangemColorPalette.Light1V2,
|
||||||
)
|
)
|
||||||
val controls = TangemColors2.Controls(
|
val controls = TangemColors2.Controls(
|
||||||
backgroundChecked = TangemColorPalette.Dark6,
|
backgroundChecked = TangemColorPalette.Dark6,
|
||||||
|
|
@ -154,16 +154,28 @@ private fun lightThemeColors2(): TangemColors2 {
|
||||||
iconGray = TangemColorPalette.Dark1,
|
iconGray = TangemColorPalette.Dark1,
|
||||||
iconDisabled = TangemColorPalette.Light2,
|
iconDisabled = TangemColorPalette.Light2,
|
||||||
borderGray = TangemColorPalette.Light3,
|
borderGray = TangemColorPalette.Light3,
|
||||||
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
backgroundTintedBlue = TangemColorPalette.Azure_10,
|
||||||
textBlue = text.status.accent,
|
textBlue = text.status.accent,
|
||||||
backgroundSolidRed = TangemColorPalette.Amaranth,
|
backgroundSolidRed = TangemColorPalette.Amaranth,
|
||||||
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
backgroundTintedRed = TangemColorPalette.Amaranth_10,
|
||||||
iconBlue = TangemColorPalette.Azure,
|
iconBlue = TangemColorPalette.Azure,
|
||||||
iconRed = TangemColorPalette.Amaranth,
|
iconRed = TangemColorPalette.Amaranth,
|
||||||
textRed = TangemColorPalette.Amaranth,
|
textRed = TangemColorPalette.Amaranth,
|
||||||
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
|
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
|
||||||
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
borderTintedBlue = TangemColorPalette.Azure_10,
|
||||||
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
borderTintedRed = TangemColorPalette.Amaranth_10,
|
||||||
|
textGreen = TangemColorPalette.Emerald,
|
||||||
|
iconGreen = TangemColorPalette.Emerald,
|
||||||
|
borderTintedGreen = TangemColorPalette.Emerald_10,
|
||||||
|
borderSolidColor = TangemColorPalette.Emerald_50,
|
||||||
|
backgroundTintedGreen = TangemColorPalette.Emerald_10,
|
||||||
|
backgroundSolidGreen = TangemColorPalette.Emerald,
|
||||||
|
textGreenAlt = TangemColorPalette.Eucalyptus,
|
||||||
|
iconGreenAlt = TangemColorPalette.Eucalyptus,
|
||||||
|
borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
|
||||||
|
borderSolidColorAlt = TangemColorPalette.Eucalyptus_50,
|
||||||
|
backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
|
||||||
|
backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus,
|
||||||
)
|
)
|
||||||
val tabs = TangemColors2.Tabs(
|
val tabs = TangemColors2.Tabs(
|
||||||
textPrimary = TangemColorPalette.Light2,
|
textPrimary = TangemColorPalette.Light2,
|
||||||
|
|
@ -270,8 +282,8 @@ private fun darkThemeColors2(): TangemColors2 {
|
||||||
borderPrimary = TangemColorPalette.Light4,
|
borderPrimary = TangemColorPalette.Light4,
|
||||||
)
|
)
|
||||||
val surface = TangemColors2.Surface(
|
val surface = TangemColors2.Surface(
|
||||||
level1 = TangemColorPalette.Dark6,
|
level1 = TangemColorPalette.Black,
|
||||||
level2 = TangemColorPalette.Black,
|
level2 = TangemColorPalette.Dark7,
|
||||||
level3 = TangemColorPalette.Dark6,
|
level3 = TangemColorPalette.Dark6,
|
||||||
level4 = TangemColorPalette.Dark5,
|
level4 = TangemColorPalette.Dark5,
|
||||||
)
|
)
|
||||||
|
|
@ -304,7 +316,7 @@ private fun darkThemeColors2(): TangemColors2 {
|
||||||
iconGray = TangemColorPalette.Dark2,
|
iconGray = TangemColorPalette.Dark2,
|
||||||
iconDisabled = TangemColorPalette.Dark5,
|
iconDisabled = TangemColorPalette.Dark5,
|
||||||
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
|
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
|
||||||
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
backgroundTintedBlue = TangemColorPalette.Azure_10,
|
||||||
textBlue = text.status.accent,
|
textBlue = text.status.accent,
|
||||||
backgroundSolidRed = TangemColorPalette.Amaranth,
|
backgroundSolidRed = TangemColorPalette.Amaranth,
|
||||||
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
||||||
|
|
@ -312,8 +324,20 @@ private fun darkThemeColors2(): TangemColors2 {
|
||||||
iconRed = TangemColorPalette.Flamingo,
|
iconRed = TangemColorPalette.Flamingo,
|
||||||
textRed = TangemColorPalette.Flamingo,
|
textRed = TangemColorPalette.Flamingo,
|
||||||
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
|
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
|
||||||
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
|
borderTintedBlue = TangemColorPalette.Azure_10,
|
||||||
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
|
borderTintedRed = TangemColorPalette.Amaranth_10,
|
||||||
|
textGreen = TangemColorPalette.Emerald,
|
||||||
|
iconGreen = TangemColorPalette.Emerald,
|
||||||
|
borderTintedGreen = TangemColorPalette.Emerald_10,
|
||||||
|
borderSolidColor = TangemColorPalette.Emerald_50,
|
||||||
|
backgroundTintedGreen = TangemColorPalette.Emerald_10,
|
||||||
|
backgroundSolidGreen = TangemColorPalette.Emerald,
|
||||||
|
textGreenAlt = TangemColorPalette.Emerald,
|
||||||
|
iconGreenAlt = TangemColorPalette.Emerald,
|
||||||
|
borderTintedGreenAlt = TangemColorPalette.Emerald_10,
|
||||||
|
borderSolidColorAlt = TangemColorPalette.Emerald_50,
|
||||||
|
backgroundTintedGreenAlt = TangemColorPalette.Emerald_10,
|
||||||
|
backgroundSolidGreenAlt = TangemColorPalette.Emerald,
|
||||||
)
|
)
|
||||||
val tabs = TangemColors2.Tabs(
|
val tabs = TangemColors2.Tabs(
|
||||||
textPrimary = TangemColorPalette.Dark4,
|
textPrimary = TangemColorPalette.Dark4,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import com.tangem.core.ui.UiDependencies
|
import com.tangem.core.ui.UiDependencies
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemeRedesign
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing a Compose screen with common theming and content composition properties.
|
* Interface representing a Compose screen with common theming and content composition properties.
|
||||||
|
|
@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView(
|
||||||
uiDependencies = uiDependencies,
|
uiDependencies = uiDependencies,
|
||||||
overrideSystemBarColors = overrideSystemBarColors,
|
overrideSystemBarColors = overrideSystemBarColors,
|
||||||
) {
|
) {
|
||||||
ScreenContent(modifier = screenModifier)
|
TangemThemeRedesign {
|
||||||
|
ScreenContent(modifier = screenModifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.tangem.core.ui.shader
|
||||||
|
|
||||||
|
class GlossyShader : TangemShader {
|
||||||
|
override val sksl: String =
|
||||||
|
"""
|
||||||
|
// The MIT License
|
||||||
|
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
uniform float uTime;
|
||||||
|
uniform vec3 uResolution;
|
||||||
|
|
||||||
|
vec4 main( vec2 fragCoord )
|
||||||
|
{
|
||||||
|
float mr = min(uResolution.x, uResolution.y);
|
||||||
|
vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr;
|
||||||
|
|
||||||
|
float d = -uTime * 0.5;
|
||||||
|
float a = 0.0;
|
||||||
|
for (float i = 0.0; i < 8.0; ++i) {
|
||||||
|
a += cos(i - d - a * uv.x);
|
||||||
|
d += sin(uv.y * i + a);
|
||||||
|
}
|
||||||
|
d += uTime * 0.5;
|
||||||
|
vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
|
||||||
|
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5);
|
||||||
|
return vec4(col,1.0);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
package com.tangem.core.ui.shader
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.tangem.core.ui.shader.runtime.RuntimeEffect
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shader that creates a colorful, flowing "northern lights" effect.
|
||||||
|
* @param colors The colors to display. The last provided color acts like a "background"
|
||||||
|
* @param speed Adjust the speed of the movement
|
||||||
|
* @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
class NorthernLightsMeshGradientShader(
|
||||||
|
colors: Array<Color>,
|
||||||
|
speed: Float = 1f,
|
||||||
|
scale: Float = 2f,
|
||||||
|
) : TangemShader {
|
||||||
|
|
||||||
|
private val colorCount = colors.size
|
||||||
|
private val colorUniforms = colors.flatMap {
|
||||||
|
listOf(it.red, it.green, it.blue)
|
||||||
|
}.toTypedArray().toFloatArray()
|
||||||
|
private val ambientUniform = FloatArray(3)
|
||||||
|
|
||||||
|
init {
|
||||||
|
recomputeAmbient()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val sksl = """
|
||||||
|
uniform float uTime;
|
||||||
|
uniform vec3 uResolution;
|
||||||
|
uniform vec3 uAmbient;
|
||||||
|
|
||||||
|
const int MAX_COLORS = $colorCount;
|
||||||
|
uniform vec3 uColor[MAX_COLORS];
|
||||||
|
|
||||||
|
// Simplex 3D Noise
|
||||||
|
// by Ian McEwan, Ashima Arts
|
||||||
|
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
|
||||||
|
//
|
||||||
|
vec4 permute(vec4 x) {
|
||||||
|
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
||||||
|
}
|
||||||
|
vec4 taylorInvSqrt(vec4 r) {
|
||||||
|
return 1.79284291400159 - 0.85373472095314 * r;
|
||||||
|
}
|
||||||
|
|
||||||
|
float snoise(vec3 v) {
|
||||||
|
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
|
||||||
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||||||
|
|
||||||
|
// First corner
|
||||||
|
vec3 i = floor(v + dot(v, C.yyy));
|
||||||
|
vec3 x0 = v - i + dot(i, C.xxx);
|
||||||
|
|
||||||
|
// Other corners
|
||||||
|
vec3 g = step(x0.yzx, x0.xyz);
|
||||||
|
vec3 l = 1.0 - g;
|
||||||
|
vec3 i1 = min(g.xyz, l.zxy);
|
||||||
|
vec3 i2 = max(g.xyz, l.zxy);
|
||||||
|
|
||||||
|
// x0 = x0 - 0. + 0.0 * C
|
||||||
|
vec3 x1 = x0 - i1 + 1.0 * C.xxx;
|
||||||
|
vec3 x2 = x0 - i2 + 2.0 * C.xxx;
|
||||||
|
vec3 x3 = x0 - 1. + 3.0 * C.xxx;
|
||||||
|
|
||||||
|
// Permutations
|
||||||
|
i = mod(i, 289.0);
|
||||||
|
vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
||||||
|
|
||||||
|
// Gradients
|
||||||
|
// ( N*N points uniformly over a square, mapped onto an octahedron.)
|
||||||
|
float n_ = 1.0 / 7.0; // N=7
|
||||||
|
vec3 ns = n_ * D.wyz - D.xzx;
|
||||||
|
|
||||||
|
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N)
|
||||||
|
|
||||||
|
vec4 x_ = floor(j * ns.z);
|
||||||
|
vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
|
||||||
|
|
||||||
|
vec4 x = x_ * ns.x + ns.yyyy;
|
||||||
|
vec4 y = y_ * ns.x + ns.yyyy;
|
||||||
|
vec4 h = 1.0 - abs(x) - abs(y);
|
||||||
|
|
||||||
|
vec4 b0 = vec4(x.xy, y.xy);
|
||||||
|
vec4 b1 = vec4(x.zw, y.zw);
|
||||||
|
|
||||||
|
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
||||||
|
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
||||||
|
vec4 sh = -step(h, vec4(0.0));
|
||||||
|
|
||||||
|
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
||||||
|
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
||||||
|
|
||||||
|
vec3 p0 = vec3(a0.xy, h.x);
|
||||||
|
vec3 p1 = vec3(a0.zw, h.y);
|
||||||
|
vec3 p2 = vec3(a1.xy, h.z);
|
||||||
|
vec3 p3 = vec3(a1.zw, h.w);
|
||||||
|
|
||||||
|
//Normalise gradients
|
||||||
|
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
|
||||||
|
p0 *= norm.x;
|
||||||
|
p1 *= norm.y;
|
||||||
|
p2 *= norm.z;
|
||||||
|
p3 *= norm.w;
|
||||||
|
|
||||||
|
// Mix final noise value
|
||||||
|
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
|
||||||
|
m = m * m;
|
||||||
|
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 main( vec2 fragCoord ) {
|
||||||
|
float mr = min(uResolution.x, uResolution.y);
|
||||||
|
vec2 uv = (fragCoord * $scale - uResolution.xy) / mr;
|
||||||
|
|
||||||
|
vec2 base = uv / 2;
|
||||||
|
|
||||||
|
vec3 vColor = uColor[MAX_COLORS - 1];
|
||||||
|
|
||||||
|
const vec2 frequency = vec2(0.7, 0.3);
|
||||||
|
const float noiseFloor = 0.00001;
|
||||||
|
float t = uTime * 0.005;
|
||||||
|
|
||||||
|
for(int i = 0; i < MAX_COLORS - 1; i++) {
|
||||||
|
float fi = float(i);
|
||||||
|
float flow = 5. + fi * 0.3;
|
||||||
|
float speed = 6. * $speed + fi * 0.3;
|
||||||
|
float seed = 1. + fi * 4.;
|
||||||
|
float noiseCeil = 0.6 + fi * 0.07;
|
||||||
|
|
||||||
|
float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed)));
|
||||||
|
|
||||||
|
vColor = mix(vColor, uColor[i], noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
vColor = max(vColor, uAmbient);
|
||||||
|
|
||||||
|
// Elliptical falloff centred at the very top of the screen.
|
||||||
|
// Using fragCoord directly (pixels) and uResolution for screen size.
|
||||||
|
// Horizontal radius ~ 80 % of screen width → wide enough to cover corners.
|
||||||
|
// Vertical radius ~ 45 % of screen height → controls how far down the glow reaches.
|
||||||
|
vec2 topCenter = vec2(uResolution.x * 0.5, 0.0);
|
||||||
|
vec2 delta = fragCoord - topCenter;
|
||||||
|
vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65);
|
||||||
|
float normDist = length(delta / radii);
|
||||||
|
float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5);
|
||||||
|
|
||||||
|
// Pre-multiplied alpha so the shader composites correctly over the dark background.
|
||||||
|
return vec4(vColor * alpha, alpha);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/** Updates the animated colors in-place without recreating the shader. */
|
||||||
|
fun updateColors(colors: Array<Color>) {
|
||||||
|
colors.forEachIndexed { i, color ->
|
||||||
|
colorUniforms[i * 3 + 0] = color.red
|
||||||
|
colorUniforms[i * 3 + 1] = color.green
|
||||||
|
colorUniforms[i * 3 + 2] = color.blue
|
||||||
|
}
|
||||||
|
recomputeAmbient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recomputeAmbient() {
|
||||||
|
val count = colorCount - 1
|
||||||
|
var r = 0f
|
||||||
|
var g = 0f
|
||||||
|
var b = 0f
|
||||||
|
for (i in 0 until count) {
|
||||||
|
r += colorUniforms[i * 3]
|
||||||
|
g += colorUniforms[i * 3 + 1]
|
||||||
|
b += colorUniforms[i * 3 + 2]
|
||||||
|
}
|
||||||
|
val scale = 0.5f / count
|
||||||
|
ambientUniform[0] = r * scale
|
||||||
|
ambientUniform[1] = g * scale
|
||||||
|
ambientUniform[2] = b * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
|
||||||
|
super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height)
|
||||||
|
|
||||||
|
runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms)
|
||||||
|
runtimeEffect.setFloatUniform(
|
||||||
|
name = "uAmbient",
|
||||||
|
value1 = ambientUniform[0],
|
||||||
|
value2 = ambientUniform[1],
|
||||||
|
value3 = ambientUniform[2],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.tangem.core.ui.shader
|
||||||
|
|
||||||
|
import com.tangem.core.ui.shader.runtime.RuntimeEffect
|
||||||
|
|
||||||
|
interface TangemShader {
|
||||||
|
val speedModifier: Float
|
||||||
|
get() = 0.5f
|
||||||
|
|
||||||
|
val sksl: String
|
||||||
|
|
||||||
|
/** Applies the uniforms required for this shader to the effect */
|
||||||
|
fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
|
||||||
|
runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height)
|
||||||
|
runtimeEffect.setFloatUniform(name = "uTime", value1 = time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.core.ui.shader.runtime
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
internal class FallbackRuntimeEffect : RuntimeEffect {
|
||||||
|
override val isSupported: Boolean = false
|
||||||
|
override val isReady: Boolean = false
|
||||||
|
|
||||||
|
override fun build(): Brush {
|
||||||
|
return Brush.horizontalGradient(listOf(Color.White, Color.White))
|
||||||
|
}
|
||||||
|
}
|
||||||
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