Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-29 12:53:30 +03:00
commit 82c92f8180
639 changed files with 18827 additions and 2682 deletions

View file

@ -4,8 +4,36 @@
<inspection_tool class="CanBeParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CanBePrimaryConstructorProperty" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ComplexRedundantLet" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ConstantConditionIf" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ExplicitThis" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="KotlinDoubleNegation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinRedundantOverride" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
@ -19,6 +47,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

View file

@ -74,13 +74,14 @@ dependencies {
implementation(projects.domain.walletConnect)
implementation(projects.domain.markets)
implementation(projects.domain.manageTokens)
implementation(projects.domain.onramp)
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.featuretoggles)
implementation(projects.core.configToggles)
implementation(projects.core.res)
implementation(projects.core.ui)
implementation(projects.core.datasource)
@ -112,6 +113,7 @@ dependencies {
implementation(projects.data.walletConnect)
implementation(projects.data.markets)
implementation(projects.data.manageTokens)
implementation(projects.data.onramp)
/** Features */
implementation(projects.features.onboarding)

View file

@ -749,6 +749,16 @@
}
]
},
{
"id": "chiliz",
"name": "Chiliz",
"symbol": "CHZ",
"networks": [
{
"networkId": "chiliz/test"
}
]
},
{
"id": "casper-network",
"name": "Casper",

View file

@ -3,9 +3,11 @@ package com.tangem.tap
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.connection.NetworkConnectionManager
@ -34,6 +36,7 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@ -52,6 +55,8 @@ interface ApplicationEntryPoint {
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
@ -113,4 +118,8 @@ interface ApplicationEntryPoint {
fun getOnrampFeatureToggles(): OnrampFeatureToggles
fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles
fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
fun getExcludedBlockchains(): ExcludedBlockchains
}

View file

@ -52,6 +52,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.send.api.navigation.SendRouter
@ -59,6 +60,7 @@ import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
@ -72,6 +74,7 @@ import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
@ -186,6 +189,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
@Inject
lateinit var backupServiceHolder: BackupServiceHolder
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -295,7 +304,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
} else {
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
}
lockUserWalletsTimer = LockUserWalletsTimer(
owner = this,
settingsRepository = settingsRepository,
@ -382,6 +398,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
}
@ -555,6 +572,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
// TODO add onboarding v2 handling
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}

View file

@ -9,6 +9,7 @@ import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.ParamsInterceptor
@ -17,7 +18,8 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
@ -57,9 +59,11 @@ import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -88,6 +92,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
private val excludedBlockchainsManager: ExcludedBlockchainsManager
get() = entryPoint.getExcludedBlockchainsManager()
private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager()
@ -180,6 +187,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles
get() = entryPoint.getOnboardingV2FeatureToggles()
private val dispatchers: CoroutineDispatcherProvider
get() = entryPoint.getCoroutineDispatcherProvider()
private val excludedBlockchains: ExcludedBlockchains
get() = entryPoint.getExcludedBlockchains()
// endregion
override fun onCreate() {
@ -200,9 +213,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
)
}
loadNativeLibraries()
@ -216,7 +231,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
derivationsFinder = DerivationsFinder(
appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(),
dispatchers = dispatchers,
)
appStateHolder.mainStore = store
@ -258,6 +273,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
onrampFeatureToggles = onrampFeatureToggles,
environmentConfigStorage = environmentConfigStorage,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
excludedBlockchains = excludedBlockchains,
),
),
)

View file

@ -140,6 +140,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
message = state.dialog.error.message,
context = context,
)
is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)

View file

@ -1,25 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class MainScreen(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Main Screen", event, params) {
class ScreenOpened : MainScreen("Screen opened")
class ButtonScanCard : MainScreen("Button - Scan Card")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Push(event: String) : AnalyticsEvent(
category = "Push",
event = event,
params = emptyMap(),
error = null,
) {
data object PushNotificationOpened : Push(event = "Push Notification Opened")
}

View file

@ -11,7 +11,7 @@ sealed class SignIn(
error: Throwable? = null,
) : AnalyticsEvent("Sign In", event, params, error) {
class ScreenOpened : SignIn(event = "Sing In Screen Opened")
class ScreenOpened : SignIn(event = "Sign In Screen Opened")
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonCardSignIn : SignIn(event = "Button - Card Sign In")

View file

@ -12,11 +12,12 @@ internal sealed class WalletConnect(
) : AnalyticsEvent("Wallet Connect", event, params, error) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionEstablished(dAppName: String, dAppUrl: String) : WalletConnect(
class NewSessionEstablished(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "New Session Established",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)

View file

@ -9,7 +9,6 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -31,7 +30,7 @@ class CardContextInterceptor(
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is IntroductionProcess.ButtonScanCard, is MainScreen.ButtonScanCard -> false
is IntroductionProcess.ButtonScanCard -> false
else -> true
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.remove
fun Blockchain.getNetworkStandardName(): String {
return when (this) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
Blockchain.Tron, Blockchain.TronTestnet -> "TRC20"
Blockchain.TON -> "TON"
else -> ""
}
}
val Blockchain.networkNameWithoutTestnet
get() = this.getNetworkName().remove(" Testnet")

View file

@ -18,6 +18,7 @@ import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
import timber.log.Timber
@ -37,6 +38,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
// TODO refactoring: [REDACTED_JIRA]
val intent = Intent(applicationContext, MainActivity::class.java)
intent.putExtra(OnPushClickedIntentHandler.IS_OPENED_FROM_PUSH, true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,

View file

@ -64,12 +64,16 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
val buyService = makeBuyExchangeService(config)
val sellService = makeSellExchangeService(config)
val exchangeManager = CurrencyExchangeManager(
buyService = makeBuyExchangeService(config),
sellService = makeSellExchangeService(config),
buyService = buyService,
sellService = sellService,
primaryRules = CardExchangeRules(cardProvider),
)
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
store.inject(DaggerGraphState::appStateHolder).buyService = buyService
store.inject(DaggerGraphState::appStateHolder).sellService = sellService
store.inject(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.url
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
@ -34,6 +35,8 @@ internal class CustomTabsUrlOpener : UrlOpener {
)
.build()
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}

View file

@ -1,15 +1,21 @@
package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -39,8 +45,24 @@ internal object ActivityModule {
@Provides
@Singleton
fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager {
return DefaultRampManager(appStateHolder.exchangeService)
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
swapServiceLoader: SwapServiceLoader,
currenciesRepository: CurrenciesRepository,
getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
exchangeService = appStateHolder.exchangeService,
buyService = Provider { requireNotNull(appStateHolder.buyService) },
sellService = Provider { requireNotNull(appStateHolder.sellService) },
swapServiceLoader = swapServiceLoader,
currenciesRepository = currenciesRepository,
getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase,
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -0,0 +1,17 @@
package com.tangem.tap.di
import com.tangem.sdk.api.BackupServiceHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemSdkModule {
@Provides
@Singleton
fun provideBackupServiceHolder(): BackupServiceHolder = BackupServiceHolder()
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.data
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.sdk.api.TangemSdkManager
@ -20,8 +21,9 @@ internal object CardDataModule {
fun providesDerivationsRepository(
tangemSdkManager: TangemSdkManager,
userWalletsStore: UserWalletsStore,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
): DerivationsRepository {
return DefaultDerivationsRepository(tangemSdkManager, userWalletsStore, dispatchers)
return DefaultDerivationsRepository(tangemSdkManager, userWalletsStore, excludedBlockchains, dispatchers)
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase

View file

@ -0,0 +1,160 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object OnrampDomainModule {
@Provides
@Singleton
fun provideGetOnrampCurrenciesUseCase(onrampRepository: OnrampRepository): GetOnrampCurrenciesUseCase {
return GetOnrampCurrenciesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCurrencyUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCurrencyUseCase {
return OnrampSaveDefaultCurrencyUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampCountriesUseCase(onrampRepository: OnrampRepository): GetOnrampCountriesUseCase {
return GetOnrampCountriesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampCountryUseCase(onrampRepository: OnrampRepository): GetOnrampCountryUseCase {
return GetOnrampCountryUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCountryUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCountryUseCase {
return OnrampSaveDefaultCountryUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideCheckOnrampAvailabilityUseCase(onrampRepository: OnrampRepository): CheckOnrampAvailabilityUseCase {
return CheckOnrampAvailabilityUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampStatusUseCase(
onrampRepository: OnrampRepository,
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampStatusUseCase {
return GetOnrampStatusUseCase(
onrampRepository,
onrampTransactionRepository,
onrampErrorResolver,
)
}
@Provides
@Singleton
fun provideGetOnrampCurrencyUseCase(onrampRepository: OnrampRepository): GetOnrampCurrencyUseCase {
return GetOnrampCurrencyUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampTransactionsUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): GetOnrampTransactionsUseCase {
return GetOnrampTransactionsUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideGetOnrampTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): GetOnrampTransactionUseCase {
return GetOnrampTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideOnrampRemoveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): OnrampRemoveTransactionUseCase {
return OnrampRemoveTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideOnrampSaveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): OnrampSaveTransactionUseCase {
return OnrampSaveTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideGetOnrampPaymentMethodsUseCase(onrampRepository: OnrampRepository): GetOnrampPaymentMethodsUseCase {
return GetOnrampPaymentMethodsUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideClearOnrampCacheUseCase(onrampRepository: OnrampRepository): ClearOnrampCacheUseCase {
return ClearOnrampCacheUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampFetchQuotesUseCase(onrampRepository: OnrampRepository): OnrampFetchQuotesUseCase {
return OnrampFetchQuotesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampSelectedPaymentMethodUseCase(
onrampRepository: OnrampRepository,
): GetOnrampSelectedPaymentMethodUseCase {
return GetOnrampSelectedPaymentMethodUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(
onrampRepository: OnrampRepository,
): GetOnrampProviderWithQuoteUseCase {
return GetOnrampProviderWithQuoteUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveSelectedPaymentMethod(onrampRepository: OnrampRepository): OnrampSaveSelectedPaymentMethod {
return OnrampSaveSelectedPaymentMethod(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampRedirectUrlUseCase(
onrampRepository: OnrampRepository,
transactionRepository: OnrampTransactionRepository,
): GetOnrampRedirectUrlUseCase {
return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository)
}
}

View file

@ -133,14 +133,6 @@ internal object SettingsDomainModule {
return ShouldShowRingPromoUseCase(promoSettingsRepository)
}
@Provides
@Singleton
fun provideShouldShowTravalaPromoWalletUseCase(
promoSettingsRepository: PromoSettingsRepository,
): ShouldShowTravalaPromoWalletUseCase {
return ShouldShowTravalaPromoWalletUseCase(promoSettingsRepository)
}
@Provides
@Singleton
fun provideShouldShowSwapPromoTokenUseCase(

View file

@ -0,0 +1,23 @@
package com.tangem.tap.di.domain
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object SwapDomainModule {
@Provides
@Singleton
fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase {
return GetAvailablePairsUseCase(swapRepository = swapRepository)
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -119,26 +118,26 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
rampStateManager: RampStateManager,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
rampStateManager = rampStateManager,
)
}
@ -216,23 +215,19 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
stakingFeatureToggles: StakingFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
walletManagersFacade = walletManagersFacade,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
stakingFeatureToggles = stakingFeatureToggles,
dispatchers = dispatchers,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
@ -32,6 +33,7 @@ private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
internal class DefaultDerivationsRepository(
private val tangemSdkManager: TangemSdkManager,
private val userWalletsStore: UserWalletsStore,
private val excludedBlockchains: ExcludedBlockchains,
private val dispatchers: CoroutineDispatcherProvider,
) : DerivationsRepository {
@ -49,6 +51,7 @@ internal class DefaultDerivationsRepository(
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
extraDerivationPath = null,
scanResponse = userWallet.scanResponse,
excludedBlockchains = excludedBlockchains,
)
},
)
@ -85,6 +88,7 @@ internal class DefaultDerivationsRepository(
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
scanResponse = userWallet.scanResponse,
excludedBlockchains = excludedBlockchains,
)
},
)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain.scanCard
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
/**
* Add custom token feature toggles

View file

@ -7,6 +7,7 @@ import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -19,7 +20,6 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricU
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.*
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -1 +0,0 @@
package com.tangem.tap.domain.userWalletList.utils

View file

@ -267,7 +267,7 @@ internal class DefaultLegacyWalletConnectRepository(
)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>) {
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
val userChains = userNamespaces.flatMap { namespace ->
@ -311,6 +311,7 @@ internal class DefaultLegacyWalletConnectRepository(
WalletConnect.NewSessionEstablished(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
},

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository

View file

@ -21,7 +21,7 @@ interface LegacyWalletConnectRepository {
fun disconnect(topic: String)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>)
fun reject()

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
@ -236,8 +237,12 @@ class WalletConnectInteractor(
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId })
.map { it.fullName }
walletConnectRepository.approve(
userNamespaces = userNamespaces,
blockchainNames = blockchainNames,
)
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class WalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,

View file

@ -37,5 +37,7 @@ sealed class WalletConnectAction : Action {
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction()
data object UnsupportedDappRequest : WalletConnectAction()
//endregion WalletConnect 2.0
}

View file

@ -78,6 +78,12 @@ class WalletConnectMiddleware {
val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(uri = action.wcUri)
'1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain(
GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedWcVersion),
)
}
}
}
is WalletConnectAction.RejectRequest -> {

View file

@ -15,6 +15,7 @@ object WalletConnectReducer {
is WalletConnectAction.SessionEstablished,
is WalletConnectAction.SessionRejected,
is WalletConnectAction.PairConnectErrorAction,
is WalletConnectAction.UnsupportedDappRequest,
-> state.copy(loading = false)
is WalletConnectAction.SessionListUpdated -> state.copy(
wc2Sessions = action.sessions,

View file

@ -82,6 +82,7 @@ data class WalletForSession(
}
sealed class WalletConnectDialog : StateDialog {
data object UnsupportedWcVersion : WalletConnectDialog()
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
data object UnsupportedCard : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.home.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import javax.inject.Inject
class HomeFeatureToggles @Inject constructor(

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.features.intentHandler.IntentHandler
internal class OnPushClickedIntentHandler(val analyticsEventHandler: AnalyticsEventHandler) : IntentHandler {
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
val fromPush = intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH) ?: false
return if (fromPush) {
analyticsEventHandler.send(Push.PushNotificationOpened)
true
} else {
false
}
}
companion object {
const val IS_OPENED_FROM_PUSH = "IS_OPENED_FROM_PUSH"
}
}

View file

@ -17,7 +17,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.features.main.model.MainScreenState
@ -40,7 +39,6 @@ internal class MainViewModel @Inject constructor(
private val blockchainSDKFactory: BlockchainSDKFactory,
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
homeFeatureToggles: HomeFeatureToggles,
@ -78,9 +76,7 @@ internal class MainViewModel @Inject constructor(
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
if (stakingFeatureToggles.isStakingEnabled) {
fetchStakingTokens()
}
fetchStakingTokens()
deleteDeprecatedLogsUseCase()
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
@ -23,8 +24,8 @@ import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -67,7 +68,10 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) {
return AppRoute.Onboarding(scanResponse)
return AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = false,
)
}
return when (val type = scanResponse.productType) {
@ -88,6 +92,7 @@ object OnboardingHelper {
}
fun saveWallet(
alreadyCreatedWallet: UserWallet?,
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
@ -117,7 +122,7 @@ object OnboardingHelper {
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
@ -131,7 +136,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
}
}
@ -207,7 +212,9 @@ object OnboardingHelper {
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) {
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains)
val cryptoCurrency = CryptoCurrencyFactory(excludedBlockchains).createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
@ -242,9 +249,10 @@ object OnboardingHelper {
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
alreadyCreatedWallet: UserWallet? = null,
) {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()

View file

@ -197,11 +197,11 @@ private fun handleWalletAction(action: Action) {
}
}
private fun handleFinishBackup(scanResponse: ScanResponse) {
private fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build()
if (backupState.hasRing && userWalletId != null) {
scope.launch {
store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId)
@ -209,6 +209,7 @@ private fun handleFinishBackup(scanResponse: ScanResponse) {
}
OnboardingHelper.saveWallet(
alreadyCreatedWallet = userWallet,
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
@ -679,7 +680,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
handleFinishBackup(requireNotNull(scanResponse))
handleFinishBackup(requireNotNull(scanResponse), userWallet)
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
}
@ -740,6 +741,7 @@ private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: Ba
return requireNotNull(
value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
.backupCardsIds(backupState.backupCardIds.toSet())
.hasBackupError(backupState.hasBackupError)
.build(),
lazyMessage = { "User wallet not created" },
)

View file

@ -272,6 +272,7 @@ class OnboardingWalletFragment :
onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create(
context = onboardingComponentContext,
params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId),
onDone = {},
)
}

View file

@ -5,11 +5,11 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
@ -110,7 +110,11 @@ internal class SaveWalletMiddleware {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
state = com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On,
),
)
}
/*
@ -148,7 +152,11 @@ internal class SaveWalletMiddleware {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off))
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off,
),
)
}
}

View file

@ -55,11 +55,15 @@ object TradeCryptoMiddleware {
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled
if (isOnrampEnabled) proceedWithOnramp() else proceedWithLegacyBuyAction(state, action)
if (isOnrampEnabled) {
proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency)
} else {
proceedWithLegacyBuyAction(state, action)
}
}
private fun proceedWithOnramp() {
store.dispatchNavigationAction { push(AppRoute.Onramp) }
private fun proceedWithOnramp(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency) {
store.dispatchNavigationAction { push(AppRoute.Onramp(userWalletId = userWalletId, currency = cryptoCurrency)) }
}
private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.extensions.inject
@ -10,9 +11,11 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.converter.TwoWayConverter
internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
internal class CryptoCurrencyConverter(
private val excludedBlockchains: ExcludedBlockchains,
) : TwoWayConverter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override fun convert(value: Currency): CryptoCurrency {
return when (value) {

View file

@ -8,6 +8,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
@ -19,6 +21,8 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
/**
@ -30,9 +34,19 @@ class CurrencyExchangeManager(
private val primaryRules: ExchangeRules,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {
_initializationStatus.value = lceLoading()
buyService.update()
sellService.update()
_initializationStatus.value = lceContent()
}
override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed()

View file

@ -1,22 +1,128 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager {
@Suppress("LongParameterList")
internal class DefaultRampManager(
private val exchangeService: ExchangeService?,
private val buyService: Provider<ExchangeService>,
private val sellService: Provider<ExchangeService>,
private val swapServiceLoader: SwapServiceLoader,
private val currenciesRepository: CurrenciesRepository,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean {
override fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForSell(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
override suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean {
val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency)
if (!sellSupportedByService) return false
val reason = getSendUnavailabilityReason(userWalletId, status)
return reason == ScenarioUnavailabilityReason.None
}
override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return buyService.invoke().initializationStatus
}
override suspend fun fetchBuyServiceData() {
withContext(dispatchers.io) {
buyService.invoke().update()
}
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
override suspend fun fetchSellServiceData() {
withContext(dispatchers.io) {
sellService.invoke().update()
}
}
override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return swapServiceLoader.getInitializationStatus(userWalletId)
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress
?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = swapServiceLoader.getInitializationStatus(userWalletId).value.getOrNull()?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
private suspend fun getSendUnavailabilityReason(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason {
val coinStatus = getNetworkCoinStatusUseCase.invokeSync(
userWalletId = userWalletId,
networkId = cryptoCurrencyStatus.currency.network.id,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
isSingleWalletWithTokens = false,
).getOrNull()
return when {
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
}
currenciesRepository.isSendBlockedByPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
) -> {
ScenarioUnavailabilityReason.PendingTransaction(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
networkName = coinStatus?.currency?.network?.name.orEmpty(),
)
}
else -> {
ScenarioUnavailabilityReason.None
}
}
}
}

View file

@ -1,8 +1,14 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
interface Exchanger {
fun isBuyAllowed(): Boolean
@ -12,10 +18,17 @@ interface Exchanger {
}
interface ExchangeService : Exchanger, ExchangeUrlBuilder {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
suspend fun update()
companion object {
fun dummy(): ExchangeService = object : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false

View file

@ -138,6 +138,8 @@ internal val Blockchain.mercuryoNetwork: String?
Blockchain.CasperTestnet -> null
Blockchain.Core -> null
Blockchain.CoreTestnet -> null
Blockchain.Chiliz -> null
Blockchain.ChilizTestnet -> null
Blockchain.Unknown -> null
Blockchain.Xodex -> null
Blockchain.Canxium -> null

View file

@ -6,12 +6,19 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
/**
@ -19,10 +26,34 @@ import java.util.concurrent.CopyOnWriteArrayList
*/
internal class MercuryoService(private val environment: MercuryoEnvironment) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MercuryoApi = environment.mercuryoApi
private val availableMercuryoCurrencies = CopyOnWriteArrayList<MercuryoCurrenciesResponse.MercuryoCryptoCurrency>()
override suspend fun update() {
Timber.i("Start updating")
_initializationStatus.value = lceLoading()
val result = performRequest { api.currencies(environment.apiVersion) }
when {
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
handleSuccessfullyUpdatedData(data = result.data.data)
}
result is Result.Failure -> {
availableMercuryoCurrencies.clear()
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
}
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
@ -42,18 +73,6 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
override fun availableForSell(currency: Currency): Boolean = false
override suspend fun update() {
val result = performRequest { api.currencies(environment.apiVersion) }
when {
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
handleSuccessfullyUpdatedData(data = result.data.data)
}
result is Result.Failure -> {
availableMercuryoCurrencies.clear()
}
}
}
override fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
@ -89,6 +108,9 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) {
availableMercuryoCurrencies.clear()
availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies)
Timber.i("Successfully updated")
_initializationStatus.value = lceContent()
}
private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase()

View file

@ -7,13 +7,20 @@ import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -23,6 +30,12 @@ class MoonPayService(
private val logEnabled: Boolean,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MoonPayApi by lazy {
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
@ -34,14 +47,25 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
Timber.i("Start updating")
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Failure -> {
Timber.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Failure -> {
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
@ -58,6 +82,8 @@ class MoonPayService(
)
}
Timber.i("Successfully updated")
_initializationStatus.value = lceContent()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
}

View file

@ -43,6 +43,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Filecoin -> MoonPaySupportedCurrency(networkCode = "filecoin", currencyCode = "fil")
Sui -> MoonPaySupportedCurrency(networkCode = "sui", currencyCode = "sui")
Core -> MoonPaySupportedCurrency(networkCode = "core", currencyCode = "core")
Chiliz -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "chz")
ArbitrumTestnet -> null
AvalancheTestnet -> null
BinanceTestnet -> null
@ -139,6 +140,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Casper -> null
CasperTestnet -> null
CoreTestnet -> null
ChilizTestnet -> null
Unknown -> null
Xodex -> null
Canxium -> null

View file

@ -1,6 +1,5 @@
package com.tangem.tap.proxy
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
@ -26,10 +25,8 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder {
var mainStore: Store<AppState>? = null
var tangemSdkManager: TangemSdkManager? = null
var exchangeService: ExchangeService? = null
fun getActualCard(): CardDTO? {
return scanResponse?.card
}
var buyService: ExchangeService? = null
var sellService: ExchangeService? = null
override fun dispatch(action: Action) {
mainStore?.dispatch(action)

View file

@ -2,6 +2,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
@ -71,4 +72,5 @@ data class DaggerGraphState(
val onrampFeatureToggles: OnrampFeatureToggles? = null,
val environmentConfigStorage: EnvironmentConfigStorage? = null,
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
val excludedBlockchains: ExcludedBlockchains? = null,
) : StateType

View file

@ -27,7 +27,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
override val stack: Value<ChildStack<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
serializer = AppRoute.serializer(),
serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check
initialConfiguration = getInitialRoute(),
handleBackButton = false,
childFactory = ::child,

View file

@ -11,10 +11,8 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.onramp.component.BuyCryptoComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.component.SellCryptoComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
@ -51,8 +49,10 @@ internal class ChildFactory @Inject constructor(
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val onrampComponentFactory: OnrampComponent.Factory,
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
@ -197,10 +197,17 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Onramp -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampComponent.Params(),
params = OnrampComponent.Params(userWalletId = route.userWalletId, cryptoCurrency = route.currency),
componentFactory = onrampComponentFactory,
)
}
is AppRoute.OnrampSuccess -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.txId),
componentFactory = onrampSuccessComponentFactory,
)
}
is AppRoute.BuyCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
@ -215,10 +222,20 @@ internal class ChildFactory @Inject constructor(
componentFactory = sellCryptoComponentFactory,
)
}
is AppRoute.SwapCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
componentFactory = swapSelectTokensComponentFactory,
)
}
is AppRoute.Onboarding -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(route.scanResponse),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
),
componentFactory = onboardingEntryComponentFactory,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetworkDerivationPath
@ -17,7 +18,7 @@ import com.tangem.domain.tokens.model.Network
*/
internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
private val factory = CryptoCurrencyFactory()
private val factory = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains())
val cardano by lazy { listOf(createCoin(blockchain = Blockchain.Cardano)) }
val chia by lazy { listOf(element = createCoin(Blockchain.Chia)) }

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.card
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.ScanCardException
@ -29,6 +30,7 @@ internal class DefaultDerivationsRepositoryTest {
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
excludedBlockchains = ExcludedBlockchains(),
)
private val defaultUserWalletId = UserWalletId("011")

View file

@ -219,16 +219,24 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
val currency: CryptoCurrency,
val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"),
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
@ -287,7 +295,19 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object Onramp : AppRoute(path = "/onramp")
data class Onramp(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class OnrampSuccess(
val txId: String,
) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class BuyCrypto(
@ -299,6 +319,15 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
@Serializable
data class SwapCrypto(
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
// Onboarding V2
data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2")
@Serializable
data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean,
) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}")
}

View file

@ -31,10 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@ -118,7 +115,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
@Composable
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
FooterContainer(
footer = stringResource(id = R.string.give_permission_policy_type_footer),
footer = resourceReference(R.string.give_permission_policy_type_footer),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AmountItem(
@ -130,7 +127,7 @@ private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest)
}
SpacerH16()
FooterContainer(
footer = data.footerText.resolveReference(),
footer = data.footerText,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
FeeItem(fee = data.fee)

View file

@ -0,0 +1,228 @@
package com.tangem.common.ui.expressStatus
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Block with express statuses
*
* @param state ui holder
* @param modifier modifier
* @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4)
*/
@Composable
fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) {
val link = remember(this) { state.link as ExpressLinkUM.Content }
Row(
modifier = Modifier.clickable { link.onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = link.icon),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = link.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
Column {
state.statuses.forEachIndexed { index, item ->
ExpressStatusStep(item, index == state.statuses.lastIndex)
}
}
}
}
@Composable
private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) {
AnimatedContent(
targetState = status,
label = "Exchange Step Change Success",
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
) { content ->
Row {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (content.state) {
ExpressStatusItemState.Active -> StepInProgress()
ExpressStatusItemState.Default -> StepDefault()
ExpressStatusItemState.Done -> Step(
iconRes = R.drawable.ic_check_24,
iconColor = TangemTheme.colors.icon.primary1,
borderColor = TangemTheme.colors.field.focused,
)
ExpressStatusItemState.Error -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.warning,
)
ExpressStatusItemState.Warning -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.attention,
)
}
if (!isLast) {
StepSeparator()
}
}
val textColor = when (status.state) {
ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled
ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Error -> TangemTheme.colors.text.warning
ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention
}
Text(
text = content.text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun StepDefault() {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = borderColor,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun StepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun StepSeparator() {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.size(
width = TangemTheme.dimens.size1_5,
height = TangemTheme.dimens.size10,
)
.background(
color = TangemTheme.colors.field.focused,
shape = CircleShape,
),
)
}
@Preview
@Composable
private fun Preview_ExchangeStatusBlock() {
val state = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = ExpressLinkUM.Content(
icon = R.drawable.ic_alert_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {},
),
statuses = persistentListOf(
ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done),
ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active),
ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning),
ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error),
ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default),
),
)
TangemThemePreview {
ExpressStatusBlock(state = state)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
@Composable
fun ExpressStatusNotificationBlock(state: NotificationUM?) {
AnimatedVisibility(
visible = state?.config != null,
modifier = Modifier.padding(top = 12.dp),
label = "Express Status Notification Change",
) {
val wrappedNotification = remember(this) { requireNotNull(state?.config) }
Notification(
config = wrappedNotification,
iconTint = when (state) {
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.common.ui.expressStatus.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* UI data holder for express status block
*
* @property title block title
* @property link provider web link
* @property statuses list of possible and active statuses
*/
data class ExpressStatusUM(
val title: TextReference,
val link: ExpressLinkUM,
val statuses: ImmutableList<ExpressStatusItemUM>,
)
/**
* Provider web link for express status block.
* [Empty] if no link needed
* [Content] if link is provided and displayed
*/
@Stable
sealed class ExpressLinkUM {
data object Empty : ExpressLinkUM()
data class Content(
@DrawableRes val icon: Int,
val text: TextReference,
val onClick: () -> Unit,
) : ExpressLinkUM()
}
/**
* Single status item in express status block
*/
data class ExpressStatusItemUM(
val text: TextReference,
val state: ExpressStatusItemState,
)
/**
* Available status states for express status block
*/
enum class ExpressStatusItemState {
Active,
Default,
Done,
Warning,
Error,
;
}

View file

@ -0,0 +1,28 @@
package com.tangem.common.ui.notifications
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
object ExpressNotificationsUM {
data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning(
title = resourceReference(R.string.express_exchange_notification_verification_title),
subtitle = resourceReference(R.string.express_exchange_notification_verification_text),
iconResId = R.drawable.ic_alert_triangle_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error(
title = resourceReference(R.string.express_exchange_notification_failed_title),
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
iconResId = R.drawable.ic_alert_circle_24,
buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
}

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.shorted
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import java.math.BigDecimal
sealed class NotificationUM(val config: NotificationConfig) {
@ -307,4 +308,17 @@ sealed class NotificationUM(val config: NotificationConfig) {
),
)
}
sealed interface Solana {
data class RentInfo(
private val rentInfo: CryptoCurrencyWarning.Rent,
) : Info(
title = TextReference.Res(R.string.warning_rent_fee_title),
subtitle = TextReference.Res(
id = R.string.warning_solana_rent_fee_message,
formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount),
),
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
@ -39,8 +40,8 @@ class TokenItemStateConverter(
private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = {
createFiatAmountState(it, appCurrency)
},
private val onItemClick: (CryptoCurrencyStatus) -> Unit,
private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null,
private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
@ -75,9 +76,11 @@ class TokenItemStateConverter(
subtitleState = requireNotNull(subtitleStateProvider(this)),
fiatAmountState = requireNotNull(fiatAmountStateProvider(this)),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = getFormattedAmount()),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -94,9 +97,11 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -107,8 +112,8 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -122,7 +127,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TokenItemState.TitleState.Content(text = currencyStatus.currency.name)
TokenItemState.TitleState.Content(text = stringReference(currencyStatus.currency.name))
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
@ -130,7 +135,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.TitleState.Content(
text = currencyStatus.currency.name,
text = stringReference(currencyStatus.currency.name),
hasPending = value.hasCurrentNetworkTransactions,
)
}

View file

@ -76,6 +76,9 @@ sealed class AnalyticsParam {
data object MyWallets : ScreensSources("My Wallets")
data object Token : ScreensSources("Token")
data object Stories : ScreensSources("Stories")
data object Buy : ScreensSources("Buy")
data object Swap : ScreensSources("Swap")
data object Sell : ScreensSources("Sell")
}
sealed class TxSentFrom(val value: String) {
@ -161,6 +164,12 @@ sealed class AnalyticsParam {
}
}
enum class Status(val value: String) {
Success(value = "Success"),
Error(value = "Error"),
Pending(value = "Pending"),
}
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN_PARAM = "Token"

View file

@ -0,0 +1,75 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/**
* Main screen analytics event
*
* @param event event name
* @param params params
*/
sealed class MainScreenAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreenAnalyticsEvent(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
// region Action Buttons feature
data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Buy",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Swap",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data class ButtonSell(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Sell",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Buy Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class SellTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Sell Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Swap Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Receive Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Remove Button Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
event = "Button - Close",
params = mapOf(AnalyticsParam.SOURCE to source.value),
)
// endregion
}

View file

@ -7,7 +7,7 @@ plugins {
}
android {
namespace = "com.tangem.core.featuretoggles"
namespace = "com.tangem.core.configtoggle"
}
dependencies {

View file

@ -0,0 +1,10 @@
[
{
"name": "",
"version": "undefined"
},
{
"name": "NEXA",
"version": "undefined"
}
]

View file

@ -7,10 +7,6 @@
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "5.18.0"
},
{
"name": "STAKING_ENABLED",
"version": "5.15.0"
},
{
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.17.0"
@ -25,7 +21,7 @@
},
{
"name": "MAIN_ACTION_BUTTONS_ENABLED",
"version": "undefined"
"version": "5.19.0"
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",

View file

@ -0,0 +1,8 @@
package com.tangem.core.configtoggle.blockchain
interface ExcludedBlockchainsManager {
val excludedBlockchainsIds: Set<String>
suspend fun init()
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.configtoggle.blockchain
interface MutableExcludedBlockchainsManager : ExcludedBlockchainsManager {
suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean)
fun isMatchLocalConfig(): Boolean
suspend fun recoverLocalConfig()
}

View file

@ -0,0 +1,74 @@
package com.tangem.core.configtoggle.blockchain.impl
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
internal class DefaultExcludedBlockchainsManager(
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableExcludedBlockchainsManager {
private var isInitialized: Boolean = false
private lateinit var currentExcludedBlockchainsIds: Set<String>
private lateinit var localExcludedBlockchainsIds: Set<String>
override val excludedBlockchainsIds: Set<String>
get() {
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
return currentExcludedBlockchainsIds
}
override suspend fun init() {
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
val storedExcludedBlockchainsIds = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
default = emptySet(),
)
localExcludedBlockchainsIds = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
.filterValues { isIncluded -> !isIncluded }
.keys
currentExcludedBlockchainsIds = localExcludedBlockchainsIds + storedExcludedBlockchainsIds
isInitialized = true
}
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
if (isExcluded) {
currentExcludedBlockchainsIds += mainnetId
} else {
currentExcludedBlockchainsIds -= mainnetId
}
storeCurrent()
}
override fun isMatchLocalConfig(): Boolean {
return currentExcludedBlockchainsIds == localExcludedBlockchainsIds
}
override suspend fun recoverLocalConfig() {
currentExcludedBlockchainsIds = localExcludedBlockchainsIds
storeCurrent()
}
private suspend fun storeCurrent() {
appPreferencesStore.store(
PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
currentExcludedBlockchainsIds,
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.core.configtoggle.di
import android.content.Context
import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ExcludedBlockchainsManagerModule {
@Provides
@Singleton
fun provideExcludedBlockchainsManager(
@ApplicationContext context: Context,
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): ExcludedBlockchainsManager {
val localTogglesStorage = LocalTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return DefaultExcludedBlockchainsManager(
localTogglesStorage,
appPreferencesStore,
versionProvider,
)
}
@Provides
@Singleton
fun provideMutableExcludedBlockchainsManager(
manager: ExcludedBlockchainsManager,
): MutableExcludedBlockchainsManager? {
if (!BuildConfig.TESTER_MENU_ENABLED) return null
return manager as MutableExcludedBlockchainsManager
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.core.featuretoggle.di
package com.tangem.core.configtoggle.di
import android.content.Context
import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
@ -27,18 +27,18 @@ internal object FeatureTogglesManagerModule {
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader)
val localTogglesStorage = LocalTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider,
)
} else {
ProdFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
versionProvider = versionProvider,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature
/**
* Component for getting information about the availability of feature toggles

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature
/**
* Component for change information about the availability of feature toggles

View file

@ -1,9 +1,10 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
@ -13,12 +14,12 @@ import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property localTogglesStorage local feature toggles storage
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
@ -27,13 +28,13 @@ internal class DevFeatureTogglesManager(
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
val localFeatureToggles = localFeatureTogglesStorage.featureToggles
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
localFeatureTogglesMap = localFeatureToggles

View file

@ -0,0 +1,6 @@
package com.tangem.core.configtoggle.feature.impl
internal object FeatureTogglesConstants {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}

View file

@ -1,27 +1,28 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in PROD build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property localTogglesStorage local feature toggles storage
* @property versionProvider application version provider
*/
internal class ProdFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val localTogglesStorage: TogglesStorage,
private val versionProvider: VersionProvider,
) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localFeatureTogglesStorage.init()
featureToggles = localFeatureTogglesStorage.featureToggles
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
featureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get() ?: "")
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.configtoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about config toggle
*
* @property name toggle name
* @property version version in which the toggle will be enabled
*
* IMPORTANT: if the version is "undefined", it means that toggle is disabled!
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = true)
internal data class ConfigToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -0,0 +1,24 @@
package com.tangem.core.configtoggle.storage
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalTogglesStorage(
private val assetLoader: AssetLoader,
) : TogglesStorage {
override var toggles: List<ConfigToggle> by Delegates.notNull()
private set
override suspend fun populate(path: String) {
toggles = assetLoader.loadList<ConfigToggle>(path)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.configtoggle.storage
/**
* Component that initializes and stores a list of feature toggles
*
[REDACTED_AUTHOR]
*/
internal interface TogglesStorage {
/** List of feature toggles */
val toggles: List<ConfigToggle>
/** Populate the storage with toggles */
suspend fun populate(path: String)
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.configtoggle.utils
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
return associate { localToggle ->
Pair(
first = localToggle.name,
second = VersionAvailabilityContract(currentVersion, localToggle.version),
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.version
package com.tangem.core.configtoggle.version
import android.content.Context
import android.content.pm.PackageManager

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.version
import androidx.annotation.VisibleForTesting
import timber.log.Timber

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.version
/**
* Version contract to evaluate availability of feature toggle

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.version
package com.tangem.core.configtoggle.version
/** Application version provider */
internal interface VersionProvider {

View file

@ -1,6 +1,7 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.contract
import com.google.common.truth.Truth
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
import org.junit.Test
/**

View file

@ -1,6 +1,7 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.contract
import com.google.common.truth.Truth
import com.tangem.core.configtoggle.version.Version
import org.junit.Test
/**

View file

@ -1,11 +1,13 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
@ -19,13 +21,13 @@ import kotlin.collections.set
[REDACTED_AUTHOR]
*/
@SuppressLint("CheckResult")
internal class DevFeatureTogglesManagerTest {
internal class DevTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val localTogglesStorage = mockk<TogglesStorage>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
appPreferencesStore = appPreferenceStore,
versionProvider = versionProvider,
)
@ -34,17 +36,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns savedFeatureTogglesMap
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -61,17 +63,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns emptyMap()
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -88,17 +90,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns null
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -111,15 +113,15 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns null
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -222,8 +224,8 @@ internal class DevFeatureTogglesManagerTest {
)
val localFeatureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}

View file

@ -1,11 +1,13 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.manager
import android.content.pm.PackageManager
import com.google.common.truth.Truth
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -13,24 +15,24 @@ import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class ProdFeatureTogglesManagerTest {
internal class ProdTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val localTogglesStorage = mockk<TogglesStorage>()
private val versionProvider = mockk<VersionProvider>()
private val manager = ProdFeatureTogglesManager(localFeatureTogglesStorage, versionProvider)
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
@Test
fun `successfully initialize storage`() = runTest {
val currentVersion = "1.0.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -40,14 +42,14 @@ internal class ProdFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns null
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -56,8 +58,8 @@ internal class ProdFeatureTogglesManagerTest {
@Test
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } throws IllegalStateException(
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } throws IllegalStateException(
"Property featureToggles should be initialized before get.",
)
@ -72,14 +74,14 @@ internal class ProdFeatureTogglesManagerTest {
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
}
coVerifyOrder { localFeatureTogglesStorage.init() }
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
verifyAll(inverse = true) { versionProvider.get() }
}
@Test
fun `failure initialize storage if versionProvider throws exception`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
runCatching { manager.init() }
@ -89,7 +91,7 @@ internal class ProdFeatureTogglesManagerTest {
}
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
}
@ -122,8 +124,8 @@ internal class ProdFeatureTogglesManagerTest {
private companion object {
val localFeatureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
val disabledFeatureToggles = mapOf(

View file

@ -1,10 +1,11 @@
package com.tangem.core.featuretoggle.storage
package com.tangem.core.configtoggle.storage
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.*
@ -16,16 +17,16 @@ import java.io.IOException
[REDACTED_AUTHOR]
*/
@SuppressLint("CheckResult")
internal class LocalFeatureTogglesStorageTest {
internal class LocalTogglesStorageTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val jsonAdapter = mockk<JsonAdapter<List<FeatureToggle>>>()
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val storage = LocalFeatureTogglesStorage(assetLoader)
private val storage = LocalTogglesStorage(assetLoader)
@Test
fun `successfully initialize storage`() = runTest {
@ -33,14 +34,14 @@ internal class LocalFeatureTogglesStorageTest {
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() returns featureToggles
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(featureToggles)
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
}
@Test
@ -49,33 +50,33 @@ internal class LocalFeatureTogglesStorageTest {
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() throws IOException()
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
}
@Test
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
everyReadingJson() throws IOException()
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
}
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
private fun everyCreatingMoshiAdapter() = every {
val types = Types.newParameterizedType(List::class.java, FeatureToggle::class.java)
moshi.adapter<List<FeatureToggle>>(types)
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
moshi.adapter<List<ConfigToggle>>(types)
}
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
@ -97,8 +98,8 @@ internal class LocalFeatureTogglesStorageTest {
""".trimIndent()
val featureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}

View file

@ -21,11 +21,13 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
/** Tangem libraries */
implementation(deps.tangem.blockchain)

View file

@ -7,9 +7,12 @@ internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<DateTime>()
} else {
val dateString = reader.nextString()
DateTime.parse(dateString)
}
}
@ToJson

View file

@ -10,8 +10,12 @@ internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
@FromJson
override fun fromJson(reader: JsonReader): LocalDate? {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<LocalDate>()
} else {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
}
}
@ToJson

View file

@ -41,4 +41,7 @@ enum class ExchangeProviderType {
@Json(name = "dex-bridge")
DEX_BRIDGE,
@Json(name = "onramp")
ONRAMP,
}

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