Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-27 15:17:39 +03:00
commit b6bf011204
1257 changed files with 31185 additions and 8936 deletions

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
@ -142,4 +145,10 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
}

View file

@ -7,6 +7,8 @@ import androidx.work.WorkerParameters
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor(
@Assisted params: WorkerParameters,
private val settingsRepository: SettingsRepository,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
Timber.i("onStart job")
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
userWalletsListManagerLockable.lock()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.lockAllWallets()
.onRight {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
} else {
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
userWalletsListManagerLockable.lock()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
Timber.i("onStart job complete")
return Result.success()
}

View file

@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
import kotlinx.coroutines.CoroutineScope
@ -25,6 +27,8 @@ internal class LockUserWalletsTimer(
private val settingsRepository: SettingsRepository,
private val duration: Duration = with(Duration) { 5.minutes },
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val coroutineScope: CoroutineScope,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -108,20 +112,33 @@ internal class LockUserWalletsTimer(
delay(duration)
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val userWallets = userWalletsListRepository.userWalletsSync()
if (userWallets.isNotEmpty()) {
userWalletsListRepository.lockAllWallets()
.onLeft {
start()
}
.onRight {
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
} else {
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
Timber.i(
"""
Timber.i(
"""
Finished
|- Millis passed: ${currentTime - startTime}
""".trimIndent(),
)
""".trimIndent(),
)
userWalletsListManager.lock()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
userWalletsListManager.lock()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
}
}

View file

@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import androidx.activity.SystemBarStyle
@ -39,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
@ -47,7 +49,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
@ -175,13 +180,31 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher
@Inject
internal lateinit var intentProcessor: IntentProcessor
@Inject
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
@Inject
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
@Inject
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
@Inject
internal lateinit var userWalletsListRepository: UserWalletsListRepository
@Inject
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -231,7 +254,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
lifecycle.addObserver(defaultDeviceFlipDetector)
if (BuildConfig.TESTER_MENU_ENABLED) {
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver)
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
}
}
@ -261,6 +284,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
settingsRepository = settingsRepository,
userWalletsListManager = userWalletsListManager,
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
hotWalletFeatureToggles = hotWalletFeatureToggles,
)
initIntentHandlers()
@ -343,12 +368,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(onPushClickedIntentHandler)
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
intentProcessor.addHandler(walletConnectLinkIntentHandler)
}
}
@ -409,11 +432,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
return if (result) super.dispatchTouchEvent(event) else false
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (BuildConfig.TESTER_MENU_ENABLED) {
testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
} else {
super.dispatchKeyEvent(event)
}
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
// TODO refactor this method to return a route instead of navigating directly
if (hotWalletFeatureToggles.isHotWalletEnabled) {
navigateToInitialScreenIfNeededNew(intentWhichStartedActivity)
return
}
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial }
@ -433,10 +469,73 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
}
@Deprecated("Refactor this method to return a route instead of navigating directly")
private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) {
lifecycleScope.launch {
val userWallets = userWalletsListRepository.userWalletsSync()
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWallets.isEmpty()) {
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = false,
)
} else {
if (userWallets.any { it.isLocked }) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
} else {
store.dispatchNavigationAction {
replaceAll(AppRoute.Wallet)
}
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = true,
)
}
if (intent != null) {
handleDeepLink(intent = intent, isFromOnNewIntent = false)
}
viewModel.checkForUnfinishedBackup()
}
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
replaceAll(AppRoute.TangemPayDetails)
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
@ -450,7 +549,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }

View file

@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
private val tangemHotSdk
get() = entryPoint.getTangemHotSdk()
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
// endregion
private val appScope = MainScope()
@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles,
),
),
)

View file

@ -1,27 +0,0 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.card.CardTypesResolver
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.utils.converter.Converter
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
/**
[REDACTED_AUTHOR]
*/
class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsParam.WalletType?> {
override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? {
if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null
} ?: return null
return CoreAnalyticsParam.WalletType.SingleCurrency(type.value)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class IntroductionProcess(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Introduction Process", event, params) {
class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
class ButtonTokensList : IntroductionProcess("Button - Tokens List")
class ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
class ButtonScanCard : IntroductionProcess("Button - Scan Card")
class ButtonRequestSupport : IntroductionProcess("Button - Request Support")
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
*/
sealed class Shop(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {
class ScreenOpened : Shop("Shop Screen Opened")
class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
event = "Purchased",
params = mapOf(
"SKU" to sku,
"Count" to count,
"Amount" to amount,
"Coupon Code" to couponCode,
).filterNotNull(),
)
class Redirected(partnerName: String?) : Shop(
event = "Redirected",
params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(),
)
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
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.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState

View file

@ -1,54 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
/**
[REDACTED_AUTHOR]
*/
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
duration: Int,
isPaused: Boolean = false,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing,
),
)
}
}
return animatable
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
/**
[REDACTED_AUTHOR]
*/
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = requireNotNull(AppCompatResources.getDrawable(LocalContext.current, drawableId)) {
"drawable is null"
}
return drawable.toBitmap().asImageBitmap()
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.sdk.extensions.pxToDp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -24,7 +24,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY])
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))

View file

@ -1,3 +0,0 @@
package com.tangem.tap.common.extensions
fun Int.isEven() = this and 1 == 0

View file

@ -3,7 +3,6 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),

View file

@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -20,7 +18,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
@ -32,7 +29,6 @@ data class AppState(
return listOf(
logMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,

View file

@ -26,10 +26,9 @@ internal object LegacyMiddleware {
{ action ->
when (action) {
is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
userWalletsListManager.selectedUserWallet
selectedUserWallet()
.distinctUntilChanged()
.onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState(
@ -52,6 +51,16 @@ internal object LegacyMiddleware {
}
}
private fun selectedUserWallet(): Flow<UserWallet> {
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
} else {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWallet
}
}
/**
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
* previously it was initialized in runBlocking and blocked details screen
@ -64,6 +73,8 @@ internal object LegacyMiddleware {
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT,
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
@ -14,8 +15,6 @@ import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -29,6 +28,7 @@ internal object ScanFailsDialog {
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
private const val RUSSIA_LOCALE = "ru"
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
@ -62,8 +62,8 @@ internal object ScanFailsDialog {
source = sourceAnalytics,
),
)
val locale = LocaleRegionProvider().getRegion()
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
val locale = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link)
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
// [REDACTED_JIRA]
@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore(
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun getAllSyncOrNull(): List<UserWallet>? {
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,

View file

@ -0,0 +1,50 @@
package com.tangem.tap.data
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class UserWalletsStoreRepositoryProxy(
private val userWalletsListRepository: UserWalletsListRepository,
) : UserWalletsStore {
override val selectedUserWalletOrNull: UserWallet?
get() = userWalletsListRepository.selectedUserWallet.value
override val userWallets: Flow<List<UserWallet>>
get() = flow {
userWalletsListRepository.load()
userWalletsListRepository.userWallets.collect {
emit(requireNotNull(it))
}
}
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListRepository.userWallets.value?.find { it.walletId == key }
}
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return catching {
val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId }
requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" }
val updatedUserWallet = update(userWallet)
userWalletsListRepository.saveWithoutLock(
userWallet = updatedUserWallet,
canOverride = true,
)
updatedUserWallet
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.tap.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
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 dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object IntentHandlingModule {
@Provides
@Singleton
fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler()
@Provides
@Singleton
fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler()
@Provides
@Singleton
fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler =
OnPushClickedIntentHandler(analyticsEventHandler)
@Provides
@Singleton
fun provideIntentProcessor(): IntentProcessor = IntentProcessor()
}

View file

@ -1,34 +0,0 @@
package com.tangem.tap.di.data
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 CardDataModule {
@Singleton
@Provides
fun providesDerivationsRepository(
tangemSdkManager: TangemSdkManager,
userWalletsStore: UserWalletsStore,
networkFactory: NetworkFactory,
dispatchers: CoroutineDispatcherProvider,
): DerivationsRepository {
return DefaultDerivationsRepository(
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
networkFactory = networkFactory,
dispatchers = dispatchers,
)
}
}

View file

@ -2,7 +2,10 @@ package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.data.RuntimeUserWalletsStore
import com.tangem.tap.data.UserWalletsStoreRepositoryProxy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,7 +18,15 @@ internal object UserWalletsStoreModule {
@Provides
@Singleton
fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore {
return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
fun provideUserWalletsStore(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UserWalletsStore {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
UserWalletsStoreRepositoryProxy(userWalletsListRepository)
} else {
RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
}
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.*
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 AccountDomainModule {
@Provides
@Singleton
fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase {
return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideUpdateCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): UpdateCryptoPortfolioUseCase {
return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideArchiveCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideGetUnoccupiedAccountIndexUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): GetUnoccupiedAccountIndexUseCase {
return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository)
}
}

View file

@ -2,13 +2,18 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.*
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.card.DefaultResetCardUseCase
@ -39,9 +44,16 @@ internal object CardDomainModule {
}
@Provides
@Singleton
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
fun provideIsNeedToBackupUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides

View file

@ -3,7 +3,9 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import com.tangem.tap.domain.scanCard.LegacyScanProcessor
@ -31,7 +33,15 @@ internal object CardLegacyDomainModule {
@Provides
@Singleton
fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(userWalletsListManager)
fun providesWalletNameGenerateUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -72,6 +73,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository,
@ -81,6 +83,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
@ -9,9 +9,12 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -63,6 +66,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
@ -71,6 +75,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -78,10 +83,14 @@ object MarketsDomainModule {
@Singleton
fun provideFilterNetworksUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
excludedBlockchains: ExcludedBlockchains,
): FilterAvailableNetworksForWalletUseCase {
return FilterAvailableNetworksForWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -154,4 +154,10 @@ internal object NFTDomainModule {
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
}
@Provides
@Singleton
fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase {
return GetNFTCurrencyUseCase(nftRepository)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.notifications.*
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.repository.PushNotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
import com.tangem.utils.notifications.PushNotificationsTokenProvider
@ -18,20 +19,22 @@ internal object NotificationsDomainModule {
@Provides
@Singleton
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase {
fun providesGetApplicationIdUseCase(
pushNotificationsRepository: PushNotificationsRepository,
): GetApplicationIdUseCase {
return GetApplicationIdUseCase(
notificationsRepository = notificationsRepository,
pushNotificationsRepository = pushNotificationsRepository,
)
}
@Provides
@Singleton
fun providesSendPushTokenUseCase(
notificationsRepository: NotificationsRepository,
pushNotificationsRepository: PushNotificationsRepository,
pushNotificationsTokenProvider: PushNotificationsTokenProvider,
): SendPushTokenUseCase {
return SendPushTokenUseCase(
notificationsRepository = notificationsRepository,
pushNotificationsRepository = pushNotificationsRepository,
pushNotificationsTokenProvider = pushNotificationsTokenProvider,
)
}
@ -56,6 +59,26 @@ internal object NotificationsDomainModule {
)
}
@Provides
@Singleton
fun providesShouldShowNotificationUseCase(
notificationsRepository: NotificationsRepository,
): ShouldShowNotificationUseCase {
return ShouldShowNotificationUseCase(
notificationsRepository = notificationsRepository,
)
}
@Provides
@Singleton
fun providesSetShouldShowNotificationUseCase(
notificationsRepository: NotificationsRepository,
): SetShouldShowNotificationUseCase {
return SetShouldShowNotificationUseCase(
notificationsRepository = notificationsRepository,
)
}
@Provides
@Singleton
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
@ -65,8 +88,8 @@ internal object NotificationsDomainModule {
@Provides
@Singleton
fun provideGetNetworksAvailableForNotifications(
notificationsRepository: NotificationsRepository,
pushNotificationsRepository: PushNotificationsRepository,
): GetNetworksAvailableForNotificationsUseCase {
return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository)
return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository)
}
}

View file

@ -94,12 +94,12 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideFetchStakingYieldBalanceUseCase(
stakingErrorResolver: StakingErrorResolver,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase(
stakingErrorResolver = stakingErrorResolver,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -173,18 +173,6 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideIsApproveNeededUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsApproveNeededUseCase {
return IsApproveNeededUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetConstructedStakingTransactionUseCase(
@ -209,12 +197,6 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase {
return GetStakingIntegrationIdUseCase(stakingRepository)
}
@Provides
@Singleton
fun provideCheckAccountInitializedUseCase(
@ -225,9 +207,13 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetActionRequirementAmountUseCase(
stakingRepository: StakingRepository,
): GetActionRequirementAmountUseCase {
return GetActionRequirementAmountUseCase(stakingRepository)
fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase {
return GetActionRequirementAmountUseCase()
}
@Provides
@Singleton
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
}
}

View file

@ -11,17 +11,19 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -46,6 +48,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
@ -54,6 +57,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -64,12 +68,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -91,11 +97,11 @@ internal object TokensDomainModule {
@Singleton
fun provideGetTokenListUseCase(
currenciesRepository: CurrenciesRepository,
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations,
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetTokenListUseCase {
return GetTokenListUseCase(
currenciesRepository = currenciesRepository,
currenciesStatusesOperations = baseCurrenciesStatusesOperations,
currenciesStatusesOperations = currenciesStatusesOperations,
)
}
@ -172,6 +178,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository,
@ -180,6 +187,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -190,12 +198,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -359,9 +369,9 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideGetWalletTotalBalanceUseCase(
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations,
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations)
return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
}
@Provides
@ -389,47 +399,12 @@ internal object TokensDomainModule {
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
}
@Provides
@Singleton
fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
)
}
@Provides
@Singleton
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -437,13 +412,14 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -451,9 +427,11 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -472,6 +450,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
return WalletBalanceFetcher(
@ -481,6 +460,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)
}
@ -490,4 +470,20 @@ internal object TokensDomainModule {
fun provideGetAssetRequirementsUseCase(walletManagersFacade: WalletManagersFacade): GetAssetRequirementsUseCase {
return GetAssetRequirementsUseCase(walletManagersFacade)
}
@Provides
@Singleton
fun provideGetViewedTokenReceiveWarningUseCase(
tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
): GetViewedTokenReceiveWarningUseCase {
return GetViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository)
}
@Provides
@Singleton
fun provideSaveViewedTokenReceiveWarningUseCase(
tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
): SaveViewedTokenReceiveWarningUseCase {
return SaveViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.data.wallets.hot.TangemHotWalletSigner
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
@ -9,9 +10,9 @@ import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.hot.TangemHotWalletSigner
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -181,8 +182,13 @@ internal object TransactionDomainModule {
fun providePrepareForSendUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): PrepareForSendUseCase {
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository)
return PrepareForSendUseCase(
transactionRepository = transactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
}
@Provides
@ -199,8 +205,13 @@ internal object TransactionDomainModule {
fun provideSignUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SignUseCase {
return SignUseCase(cardSdkConfigRepository, walletManagersFacade)
return SignUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
}
@Provides
@ -210,4 +221,24 @@ internal object TransactionDomainModule {
): CreateNFTTransferTransactionUseCase {
return CreateNFTTransferTransactionUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideGetEnsNameUseCase(
walletManagersFacade: WalletManagersFacade,
walletAddressServiceRepository: WalletAddressServiceRepository,
): GetEnsNameUseCase {
return GetEnsNameUseCase(
walletManagersFacade = walletManagersFacade,
walletAddressServiceRepository = walletAddressServiceRepository,
)
}
@Provides
@Singleton
fun provideGetReverseResolvedEnsAddressUseCase(
walletAddressServiceRepository: WalletAddressServiceRepository,
): GetReverseResolvedEnsAddressUseCase {
return GetReverseResolvedEnsAddressUseCase(walletAddressServiceRepository)
}
}

View file

@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -23,7 +25,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@ -31,18 +33,30 @@ internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
return GetWalletsUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetWalletsUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletsUseCase {
return GetWalletsUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -50,37 +64,79 @@ internal object WalletsDomainModule {
fun providesWalletNameMigrationUseCase(
userWalletsListManager: UserWalletsListManager,
walletNamesMigrationRepository: WalletNamesMigrationRepository,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): WalletNameMigrationUseCase {
return WalletNameMigrationUseCase(
userWalletsListManager = userWalletsListManager,
walletNamesMigrationRepository = walletNamesMigrationRepository,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase {
return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetUserWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetUserWalletUseCase {
return GetUserWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetSelectedWalletSyncUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager)
return GetSelectedWalletSyncUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetSelectedWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase {
return SaveWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesSaveWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
walletsRepository: WalletsRepository,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase {
return GenerateBuyTangemCardLinkUseCase()
}
@Provides
@ -99,15 +155,30 @@ internal object WalletsDomainModule {
@Singleton
fun providesSelectWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase {
return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder)
return SelectWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
reduxStateHolder = reduxStateHolder,
)
}
@Provides
@Singleton
fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase {
return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesUpdateWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UpdateWalletUseCase {
return UpdateWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -124,14 +195,30 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase {
return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetWalletsSyncUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletNamesUseCase {
return GetWalletNamesUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase {
return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesDeleteWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): DeleteWalletUseCase {
return DeleteWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -214,9 +301,13 @@ internal object WalletsDomainModule {
@Singleton
fun providesGetSavedWalletChangesIdUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSavedWalletsCountUseCase {
return GetSavedWalletsCountUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@ -261,4 +352,24 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesIsUpgradeWalletNotificationEnabledUseCase(
walletsRepository: WalletsRepository,
): IsUpgradeWalletNotificationEnabledUseCase {
return IsUpgradeWalletNotificationEnabledUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesDismissUpgradeWalletNotificationUseCase(
walletsRepository: WalletsRepository,
): DismissUpgradeWalletNotificationUseCase {
return DismissUpgradeWalletNotificationUseCase(
walletsRepository = walletsRepository,
)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.di.hot
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester
import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds
import dagger.Module
@ -17,8 +15,4 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
}

View file

@ -1,173 +0,0 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.BackendId
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
internal class DefaultDerivationsRepository(
private val tangemSdkManager: TangemSdkManager,
private val userWalletsStore: UserWalletsStore,
private val networkFactory: NetworkFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : DerivationsRepository {
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
derivePublicKeysByNetworks(
userWalletId = userWalletId,
networks = networkIds.mapNotNull {
networkFactory.create(
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
extraDerivationPath = null,
userWallet = userWallet,
)
},
)
}
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
val userWallet = withContext(dispatchers.io) {
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
}
if (userWallet is UserWallet.Hot) {
return
}
userWallet.requireColdWallet()
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
Timber.d("Nothing to derive")
return
}
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.findByNetworks(networks)
.ifEmpty {
Timber.d("Nothing to derive")
return
}
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
}
override suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (userWallet is UserWallet.Hot) {
return false
}
val derivations =
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
networkFactory.create(
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
)
},
)
return derivations.isNotEmpty()
}
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
// todo replace it in task [REDACTED_JIRA]
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId)
tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
preflightReadFilter = preflightReadFilter,
).doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
// TODO [REDACTED_TASK_KEY]
validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations)
return response.entries
}
.doOnFailure { throw it }
}
.doOnFailure { throw it }
error("This code should never be reached")
}
/**
* It throws an exception if any of the provided derivations are invalid
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
* It needs to be called after success [derivePublicKeys] or in same flows
*/
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
derivations.entries.forEach { derivationForKey ->
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
if (wallet == null) return@forEach
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
throw TangemSdkError.NonHardenedDerivationNotSupported()
}
}
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult<UserWallet> {
return withContext(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY]
)
}
}
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet {
return copy(
scanResponse = scanResponse.copy(
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
),
)
}
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
return (oldKeys.keys + newKeys.keys).toSet()
.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
}
}

View file

@ -1,124 +0,0 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
/**
* Finder of missed derivations
*
* @property scanResponse scanning response
*
[REDACTED_AUTHOR]
*/
internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
/** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks)
}
fun findByNetworks(networks: List<Network>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
networks
.mapToNewDerivations()
.forEach { data ->
val current = this[data.first]
if (current != null) {
current.addAll(data.second)
current.distinct()
} else {
this[data.first] = data.second.toMutableList()
}
}
}
}
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { network ->
val blockchain = network.toBlockchain()
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
}
}
private fun findNewDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
network: Network,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = network
.getDerivationCandidates(curve)
.ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey)
.ifEmpty { return null }
return publicKey to derivationCandidates
}
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = this.toBlockchain()
return buildList {
add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
}
.filterNotNull()
.distinct()
}
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle())
} else {
null
}
}
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
network.derivationPath.value?.let(::DerivationPath)
} else {
null
}
}
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
return if (this == Blockchain.Cardano) {
network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
}
} else {
null
}
}
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
return filterNot(alreadyDerivedPaths::contains)
}
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
return extendedPublicKeysMap.keys.toList()
}
}

View file

@ -1,113 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.common.core.TangemSdkError
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.exception.WrongPasswordException
import com.tangem.hot.sdk.model.*
import javax.inject.Inject
class HotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
) {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
HotWalletId.AuthType.Password -> requestPassword(false)
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
}
return runCatchingSdkErrors(hotWalletId, auth) {
tangemHotSdk.signHashes(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = it,
),
dataToSign = dataToSign,
).also {
hotWalletPasswordRequester.dismiss()
}
}
}
private suspend fun <T> runCatchingSdkErrors(
hotWalletId: HotWalletId,
auth: HotAuth,
block: suspend (auth: HotAuth) -> T,
): T {
return runCatchingWrongPassInternal(
originalAuth = auth,
auth = auth,
block = { blockAuth ->
block(blockAuth).also {
// TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = blockAuth,
),
auth = HotAuth.Biometry,
)
}
}
},
)
}
private suspend fun <T> runCatchingWrongPassInternal(
originalAuth: HotAuth,
auth: HotAuth,
block: suspend (auth: HotAuth) -> T,
): T = runCatching {
block(auth)
}.getOrElse { exception ->
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
// fallback to password if biometry fails
val passAuth = requestPassword(true)
return@getOrElse runCatchingWrongPassInternal(
originalAuth = originalAuth,
auth = passAuth,
block = block,
)
}
if (exception !is WrongPasswordException) {
throw exception
}
// If the exception is a wrong password, we need to request the password again
hotWalletPasswordRequester.wrongPassword()
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
runCatchingWrongPassInternal(
originalAuth = originalAuth,
auth = passResult,
block = block,
)
}
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
}
private fun Throwable.isBiometryError(): Boolean {
return this is TangemSdkError.AuthenticationFailed ||
this is TangemSdkError.AuthenticationCanceled ||
this is TangemSdkError.AuthenticationLockout ||
this is TangemSdkError.AuthenticationUnavailable ||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
this is TangemSdkError.AuthenticationNotInitialized ||
this is TangemSdkError.AuthenticationPermanentLockout
}
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
HotWalletPasswordRequester.Result.Dismiss -> null
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
interface HotWalletPasswordRequester {
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
}

View file

@ -1,78 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.map
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.operations.sign.SignData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class TangemHotSigner @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Hot,
private val hotWalletAccessor: HotWalletAccessor,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
return sign(listOf(hash), publicKey).map { it.first() }
}
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = listOf(
DataToSign(
curve = wallet.curve,
hashes = hashes,
derivationPath = publicKey.derivationPath,
),
),
)
return CompletionResult.Success(result.map { it.signatures }.flatten())
}
override suspend fun multiSign(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = dataToSign.map { signData ->
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
DataToSign(
curve = wallet.curve,
hashes = listOf(signData.hash),
derivationPath = signData.derivationPath,
)
},
)
return CompletionResult.Success(
result.mapIndexed { index, data ->
dataToSign[index].publicKey to data.signatures.first()
}.toMap(),
)
}
@AssistedFactory
interface Factory {
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
}
}

View file

@ -1,98 +0,0 @@
package com.tangem.tap.domain.hot
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.map
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.operations.sign.SignData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import timber.log.Timber
class TangemHotWalletSigner @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Hot,
private val hotWalletAccessor: HotWalletAccessor,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
return sign(listOf(hash), publicKey).map { it.first() }
}
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
val result = runCatching {
hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = listOf(
DataToSign(
curve = wallet.curve,
hashes = hashes,
derivationPath = publicKey.derivationPath,
),
),
)
}.getOrElse {
Timber.e(it)
return if (it is TangemSdkError) {
CompletionResult.Failure(it)
} else {
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
}
}
return CompletionResult.Success(result.map { it.signatures }.flatten())
}
override suspend fun multiSign(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
val result = runCatching {
hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = dataToSign.map { signData ->
val wallet =
userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
DataToSign(
curve = wallet.curve,
hashes = listOf(signData.hash),
derivationPath = signData.derivationPath,
)
},
)
}.getOrElse {
Timber.e(it)
return if (it is TangemSdkError) {
CompletionResult.Failure(it)
} else {
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
}
}
return CompletionResult.Success(
result.mapIndexed { index, data ->
dataToSign[index].publicKey to data.signatures.first()
}.toMap(),
)
}
@AssistedFactory
interface Factory {
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner
}
}

View file

@ -20,13 +20,11 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager(
): CompletionResult<VisaSignedDataByCustomerWallet> {
return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
VisaCustomerWalletApproveTask.Input(
cardId = visaDataForApprove.customerWalletCardId,
targetAddress = visaDataForApprove.targetAddress,
hashToSign = visaDataForApprove.dataToSign.hashToSign,
sign = visaDataForApprove.dataToSign::sign,
),
),
cardId = visaDataForApprove.customerWalletCardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),

View file

@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse

View file

@ -14,7 +14,7 @@ import com.tangem.common.map
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@ -10,27 +11,24 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove,
private val visaDataForApprove: Input,
) : CardSessionRunnable<VisaSignedDataByCustomerWallet> {
override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) {
@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask(
return
}
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) {
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return
}
@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask(
)
}
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
private fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask(
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signTask = SignHashCommand(
hash = hashToSign,
hash = hash,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask(
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature,
hash = hashToSign,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask(
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
)
}
is CompletionResult.Failure -> {
@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask(
}
}
}
data class Input(
val cardId: String? = null,
val targetAddress: String,
val hashToSign: String,
val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet,
)
}

View file

@ -11,14 +11,19 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
@ -26,6 +31,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -40,6 +46,7 @@ internal object UserWalletsListManagerModule {
@Provides
@Singleton
@Deprecated("Use UserWalletsListRepository instead")
fun provideGeneralUserWalletsListManager(
@ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore,
@ -58,42 +65,14 @@ internal object UserWalletsListManagerModule {
)
}
@Deprecated("Use UserWalletsListRepository instead")
private fun createBiometricUserWalletsListManager(
applicationContext: Context,
analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
@ -134,4 +113,97 @@ internal object UserWalletsListManagerModule {
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
@Provides
@Singleton
fun provideUserWalletsListRepository(
@ApplicationContext applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
passwordRequester: HotWalletPasswordRequester,
appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
dispatchers = dispatchers,
)
val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository(
moshi = moshi,
authenticatedStorage = authenticatedStorage,
dispatchers = dispatchers,
secureStorage = secureStorage,
)
return DefaultUserWalletsListRepository(
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
passwordRequester = passwordRequester,
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
tangemSdkManagerProvider = Provider { tangemSdkManager },
appPreferencesStore = appPreferencesStore,
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
)
}
fun buildMoshi(): Moshi {
return Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
}
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
return AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
}
}

View file

@ -0,0 +1,420 @@
package com.tangem.tap.domain.userWalletList.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
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.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.core.wallets.error.DeleteWalletError
import com.tangem.domain.core.wallets.error.LockWalletsError
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.core.wallets.error.SetLockError
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lock
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@Suppress("LongParameterList", "LargeClass")
internal class DefaultUserWalletsListRepository(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
private val passwordRequester: HotWalletPasswordRequester,
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
private val savePersistentInformation: ProviderSuspend<Boolean>,
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
override val selectedUserWallet = MutableStateFlow<UserWallet?>(null)
override suspend fun load() {
if (userWallets.value != null) return
if (savePersistentInformation().not()) {
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
userWallets.value = emptyList()
return
}
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { wallets ->
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
.map { wallets.updateWith(it) }
}.doOnSuccess {
userWallets.value = it
}
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId }
?: userWallets.value?.firstOrNull()
}
override suspend fun userWalletsSync(): List<UserWallet> {
load()
return requireNotNull(userWallets.value) {
"This should never happen"
}
}
override suspend fun selectedUserWalletSync(): UserWallet? {
load()
return selectedUserWallet.value
}
override suspend fun select(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SelectWalletError.UnableToSelectUserWallet)
selectedUserWalletRepository.set(userWalletId)
selectedUserWallet.value = userWallet
userWallet
}
override suspend fun saveWithoutLock(
userWallet: UserWallet,
canOverride: Boolean,
): Either<SaveWalletError, UserWallet> = either {
if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) {
raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved))
}
if (savePersistentInformation()) {
publicInformationRepository.save(userWallet, canOverride)
if (userWallet.isLocked.not()) {
sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey)
}
}
// update the userWallets state and add if it doesn't exist
userWallets.update { currentWallets ->
val wallets = currentWallets ?: emptyList()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
} else {
wallets + userWallet
}
}
// update the selectedUserWallet state if it is the only wallet
if (userWallets.value?.size == 1) {
selectedUserWalletRepository.set(userWallet.walletId)
selectedUserWallet.value = userWallet
}
userWallet
}
override suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean,
): Either<SetLockError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
removeUnsecured = changeUnsecured,
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
override suspend fun removeBiometricLock(userWalletId: UserWalletId) {
userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
if (userWalletIds.isEmpty()) return Unit.right()
publicInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
sensitiveInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
userWalletEncryptionKeysRepository.delete(userWalletIds)
val userWalletsBeforeDelete = userWallets.value ?: return@either
userWallets.update { currentWallets ->
currentWallets?.filterNot { it.walletId in userWalletIds }
}
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
userWallets.value?.findAvailableUserWallet(
userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
}
}
override suspend fun unlock(
userWalletId: UserWalletId,
unlockMethod: UserWalletsListRepository.UnlockMethod,
): Either<UnlockWalletError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(UnlockWalletError.UserWalletNotFound)
if (userWallet.isLocked.not()) {
raise(UnlockWalletError.AlreadyUnlocked)
}
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets().bind()
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
if (userWallet !is UserWallet.Hot) {
raise(UnlockWalletError.UnableToUnlock)
}
val encryptionKey = requestPasswordRecursive(
hotWalletId = userWallet.hotWalletId,
block = { password ->
runCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
}.onFailure {
raise(UnlockWalletError.UnableToUnlock)
}.getOrNull()
},
biometryFallback = {
unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
},
).bind()
if (encryptionKey == null) {
return@either
}
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
}
UserWalletsListRepository.UnlockMethod.Scan -> {
if (userWallet !is UserWallet.Cold) {
raise(UnlockWalletError.UnableToUnlock)
}
tangemSdkManagerProvider().scanProduct()
.doOnSuccess { scanResponse ->
val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (expectedId != userWallet.walletId) {
raise(UnlockWalletError.ScannedCardWalletNotMatched)
}
saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true)
.mapLeft { UnlockWalletError.UnableToUnlock }
.bind()
}
.doOnFailure {
raise(UnlockWalletError.UserCancelled)
}
}
}
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWalletIds = userWalletsSync().map { it.walletId }.toSet()
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
// TODO handle error properly [REDACTED_TASK_KEY]
raise(UnlockWalletError.UserCancelled)
}
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
val allKeys = (biometricKeys + unsecuredKeys).distinct()
val unlockedWalletsIds = allKeys.map { it.walletId }
val unlockedWallets = unlockedWalletsIds.mapNotNull { id ->
userWalletsSync().firstOrNull { it.walletId == id }
}
// Remove all password attempts for unlocked hot wallets
unlockedWallets.forEach {
removePasswordAttempts(it)
}
// if we cant unlock all wallets
if (userWalletIds.all { it in unlockedWalletsIds }.not()) {
raise(UnlockWalletError.UnableToUnlock)
}
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
userWallets.update { it?.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
override suspend fun lockAllWallets(): Either<LockWalletsError, Unit> = either {
val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet()
if (unsecuredWalletIds.size == userWallets.value?.size) {
raise(LockWalletsError.NothingToLock)
}
userWallets.update {
it?.map {
if (it.walletId !in unsecuredWalletIds) {
it.lock()
} else {
it
}
}
}
}
override suspend fun clearPersistentData() {
publicInformationRepository.clear()
sensitiveInformationRepository.clear()
userWalletEncryptionKeysRepository.clear()
}
private suspend fun requestPasswordRecursive(
hotWalletId: HotWalletId,
block: suspend (CharArray) -> UserWalletEncryptionKey?,
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
hotWalletId = hotWalletId,
authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts
hasBiometry = hasBiometry(),
)
val result = passwordRequester.requestPassword(attemptRequest)
return when (result) {
HotWalletPasswordRequester.Result.Dismiss -> {
passwordRequester.dismiss()
UnlockWalletError.UserCancelled.left()
}
is HotWalletPasswordRequester.Result.EnteredPassword -> {
val decrypted = block(result.password.value)
if (decrypted == null) {
passwordRequester.wrongPassword()
requestPasswordRecursive(hotWalletId, block, biometryFallback)
} else {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
decrypted.right()
}
}
HotWalletPasswordRequester.Result.UseBiometry -> {
biometryFallback()
.onRight {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
}
.map { null }
}
}
}
private suspend fun removePasswordAttempts(userWallet: UserWallet) {
if (userWallet is UserWallet.Hot) {
hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId)
}
}
private suspend fun hasBiometry(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
default = false,
)
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
/**
* Find the nearest available wallet that can be selected
*
* Example:
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
*
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
*
* @receiver list of user wallets without deleted wallet
*/
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
for (offset in 1..size) {
val rightIndex = prevSelectedIndex + offset
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
val leftIndex = prevSelectedIndex - offset
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
}
return lastOrNull()
}
}

View file

@ -0,0 +1,196 @@
package com.tangem.tap.domain.userWalletList.repository
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class UserWalletEncryptionKeysRepository(
moshi: Moshi,
private val authenticatedStorage: AuthenticatedStorage,
private val dispatchers: CoroutineDispatcherProvider,
private val secureStorage: SecureStorage,
) {
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
UserWalletEncryptionKey::class.java,
)
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
suspend fun save(
encryptionKey: UserWalletEncryptionKey,
removeUnsecured: Boolean = true,
method: EncryptionMethod,
) = withContext(dispatchers.io) {
if (removeUnsecured) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
}
when (method) {
EncryptionMethod.Unsecured -> {
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
is EncryptionMethod.Password -> {
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
password = method.password,
content = encryptionKey.encode(),
)
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name,
data = encodedWithPass,
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
}
storeUserWalletId(userWalletId = encryptionKey.walletId)
}
fun removeBiometricKey(userWalletId: UserWalletId) {
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
getUserWalletsIds().mapNotNull { userWalletId ->
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()
}
}
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? =
withContext(dispatchers.io) {
val encrypted = secureStorage.get(
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
) ?: return@withContext null
withContext(dispatchers.default) {
AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey()
}
}
suspend fun getAllBiometric(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
val keys = getUserWalletsIds().map { userWalletId ->
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
authenticatedStorage.get(keys).mapNotNull {
it.value.decodeToKey()
}
}
suspend fun delete(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
withContext(dispatchers.io) {
userWalletIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds }
secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name)
}
}
suspend fun clear() {
withContext(dispatchers.io) {
val userWalletsIds = getUserWalletsIds()
userWalletsIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
secureStorage.delete(StorageKey.UserWalletIds.name)
}
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.io) {
secureStorage.get(StorageKey.UserWalletIds.name)
.decodeToUserWalletsIds()
}
}
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
withContext(dispatchers.io) {
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
}
}
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(encryptionKeyAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
return withContext(dispatchers.default) {
this@decodeToKey
?.decodeToString(throwOnInvalidSequence = true)
?.let(encryptionKeyAdapter::fromJson)
}
}
private suspend fun List<UserWalletId>.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(userWalletsIdsListAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.default) {
this@decodeToUserWalletsIds
?.decodeToString(throwOnInvalidSequence = true)
?.let(userWalletsIdsListAdapter::fromJson)
.orEmpty()
}
}
sealed class EncryptionMethod {
data object Unsecured : EncryptionMethod()
data object Biometric : EncryptionMethod()
class Password(val password: CharArray) : EncryptionMethod()
}
private sealed interface StorageKey {
val name: String
class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}"
}
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
}
class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}"
}
object UserWalletIds : StorageKey {
override val name: String = "user_wallets_ids_with_saved_keys"
}
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule {
wcSessionsRepository: WalletConnectSessionsRepository,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule {
blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = coroutineDispatcherProvider,
walletConnectFeatureToggles = walletConnectFeatureToggles,
)

View file

@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.extensions.dispatchOnMain
@ -38,18 +37,14 @@ class WalletConnectInteractor(
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val userWalletsListManager: UserWalletsListManager,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
val blockchainHelper: WcBlockchainHelper,
) {
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
private var isWalletConnectReadyForDeepLinks = false
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedWalletUseCase(userWalletsListManager)
}
private val wcScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")

View file

@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -64,6 +66,14 @@ class DetailsMiddleware {
when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode(
state = state,
enable = action.enable,
)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(
state = state,
enable = action.enable,
)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -90,6 +100,91 @@ class DetailsMiddleware {
}
}
private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleRequireAccessCode(
state = state,
enable = true,
)
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all biometric sign data
removeAllBiometricSingData()
toggleSaveAccessCodes(state, enable = false)
} else {
toggleSaveAccessCodes(state, enable = true)
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.setLock(
userWalletId = it.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach {
if (it is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = it.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
@ -233,7 +328,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit)
}

View file

@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = action.enable,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
},
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isInProgress = false,
saveAccessCodes = action.prevState,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(

View file

@ -12,9 +12,14 @@ data class DetailsState(
) : StateType
data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false,
@Deprecated("Delete after hot wallet release")
val saveAccessCodes: Boolean = false,
@Deprecated("Delete after hot wallet release")
val isBiometricsAvailable: Boolean = false,
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
@ -25,5 +30,5 @@ data class AppSettingsState(
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
SaveWallets, SaveAccessCode
SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory {
onDismiss = onDismiss,
)
}
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.wallet.R
@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory {
)
}
fun createUseBiometricsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_USE_BIOMETRICS_SWITCH,
title = resourceReference(R.string.app_settings_enable_biometrics_title),
description = resourceReference(
R.string.app_settings_biometrics_footer,
wrappedList(resourceReference(R.string.common_biometrics)),
),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createRequireAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_REQUIRE_ACCESS_CODE_SWITCH,
title = resourceReference(R.string.app_settings_require_access_code),
description = resourceReference(R.string.app_settings_require_access_code_footer),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createSaveAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory {
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"
const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button"
const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch"
const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch"
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory()
@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor(
onClick = ::showAppCurrencySelector,
).let(::add)
if (state.isBiometricsAvailable) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
onCheckedChange = ::onBiometricAuthenticationToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
).let(::add)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
}
}
itemsFactory.createFlipToHideBalanceSwitch(
@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onRequireAccessCodeToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
)
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true)
@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,

View file

@ -87,9 +87,10 @@ internal class CardSettingsModel @Inject constructor(
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet $userWalletId not found") }
.requireColdWallet()
cardSdkConfigRepository.isBiometricsRequestPolicy =
userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY]
userWallet.scanResponse.card.isAccessCodeSet &&
settingsRepository.shouldSaveAccessCodes()
}
}

View file

@ -268,7 +268,7 @@ internal class ResetCardModel @Inject constructor(
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
}
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.tap.features.home
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
import com.tangem.core.ui.utils.findActivity
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.compose.StoriesScreenV2
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultHomeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
private val model: HomeModel = getOrCreateModel()
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
init {
lifecycle.subscribe(
onCreate = {
store.dispatch(HomeAction.OnCreate)
},
onStart = {
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
},
onStop = {
store.unsubscribe(this)
},
)
}
@Composable
override fun Content(modifier: Modifier) {
val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
StoriesScreenV2(
homeState = homeState,
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
onAddExistingWalletButtonClick = model::onAddExistingWalletScreen,
onScanButtonClick = model::onScanClick,
)
} else {
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
}
ChangeRootBackgroundColorEffect(Color(color = 0xFF010101))
}
override fun newState(state: HomeState) {
homeState.value = state
}
@AssistedFactory
interface Factory : HomeComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent
}
}

View file

@ -1,166 +0,0 @@
package com.tangem.tap.features.home
import androidx.compose.runtime.Stable
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class HomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val router: Router,
getUserCountryUseCase: GetUserCountryUseCase,
) : Model() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
init {
getUserCountryUseCase.invoke()
.distinctUntilChanged()
.filterNotNull()
.onEach {
val userCountry = it.getOrNull() ?: UserCountry.Other(Locale.getDefault().country)
store.dispatchOnMain(HomeAction.UserCountryLoaded(userCountry))
}
.flowOn(dispatchers.io)
.launchIn(modelScope)
}
fun onCreateNewWalletScreen() {
router.push(AppRoute.CreateWalletSelection)
}
fun onAddExistingWalletScreen() {
router.push(AppRoute.AddExistingWallet)
}
fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
scanCard()
}
fun onShopClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
analyticsEventHandler.send(Shop.ScreenOpened())
Firebase.analytics.appInstanceId
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
}
fun onSearchClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) }
}
private fun scanCard() {
modelScope.launch {
cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes()
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onFailure = {
tangemErrorHandler.onErrorReceived(error = it)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = ::proceedWithScanResponse,
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
return
}
saveWalletUseCase(userWallet).fold(
ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") },
ifRight = {
sendSignedInCardAnalyticsEvent(scanResponse)
coroutineScope { store.onUserWalletSelected(userWallet = userWallet) }
},
)
store.dispatchWithMain(HomeAction.ScanInProgress(scanInProgress = false))
delay(HIDE_PROGRESS_DELAY)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
Analytics.send(
event = Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = Basic.SignedIn.SignInType.Card,
walletsCount = "1",
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.features.home
import androidx.compose.ui.text.intl.Locale
/**
[REDACTED_AUTHOR]
*/
interface RegionProvider {
fun getRegion(): String?
}
class LocaleRegionProvider : RegionProvider {
override fun getRegion(): String = Locale.current.region
}
const val RUSSIA_COUNTRY_CODE = "ru"

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.home
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.errors.TangemSdkErrorHandler
import org.rekotlin.Store
import timber.log.Timber
class TangemTangemErrorsHandler(val store: Store<AppState>) : TangemSdkErrorHandler {
override fun onErrorReceived(error: TangemError) {
when (error) {
is TangemSdkError -> {
handleCardSdkError(error)
}
is BlockchainError -> {
handleBlockchainSdkError(error)
}
else -> {
Timber.e("Error happened", error)
}
}
}
private fun handleCardSdkError(error: TangemSdkError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> {
store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable))
}
else -> {
Timber.e(error, "Unable to scan card")
}
}
}
private fun handleBlockchainSdkError(error: TangemError) {
Timber.e("Sdk error happened", error)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.features.home.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface HomeComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, HomeComponent>
}

View file

@ -1,156 +0,0 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.toAnimatable
private const val SCALE_SWITCH_BARRIER = 1.15f
@Suppress("LongParameterList")
@Composable
fun HorizontalSlidingImage(
painter: Painter,
paused: Boolean,
duration: Int,
itemSize: DpSize,
startOffset: Float,
targetOffset: Float,
contentDescription: String,
) {
val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f)
Image(
modifier = Modifier
.requiredWidth(itemSize.width)
.requiredHeight(itemSize.height)
.graphicsLayer(
translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value,
),
alignment = Alignment.TopStart,
contentScale = ContentScale.FillBounds,
painter = painter,
contentDescription = contentDescription,
)
}
@Composable
fun StoriesTextAnimation(
slideInDuration: Int = 500,
slideInDelay: Int = 200,
slideDistance: Dp = 60.dp,
label: String = "",
content: @Composable (Modifier) -> Unit,
) {
val isLaunched = remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isLaunched.value, label = label)
val offsetY = transition.animateDp(
transitionSpec = {
tween(
durationMillis = slideInDuration,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing,
)
},
label = "Slide in",
) { value -> if (value) 0.dp else slideDistance }
val alpha = transition.animateFloat(
transitionSpec = {
tween(
durationMillis = slideInDuration * 2,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing,
)
},
label = "Visibility",
) { value -> if (value) 1f else 0f }
content(
Modifier
.absoluteOffset(y = offsetY.value)
.alpha(alpha.value),
)
LaunchedEffect(Unit) { isLaunched.value = true }
}
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
) {
val secondStepDuration = totalDuration - firstStepDuration
val isFirstStepLaunched = remember { mutableStateOf(false) }
val isSecondStepLaunched = remember { mutableStateOf(false) }
val firstTransition = updateTransition(
targetState = isFirstStepLaunched.value,
label = "Image appearing",
)
val firstScaleStep = firstTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = firstStepDuration,
easing = FastOutLinearInEasing,
)
},
label = "Appearing scale",
) { value -> if (value) secondStageScale else initialScale }
val secondTransition = updateTransition(
targetState = isSecondStepLaunched.value,
label = "Image slow outgoing",
)
val secondScaleStep = secondTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = secondStepDuration,
easing = LinearEasing,
)
},
label = "Outgoing scale",
) { value -> if (value) targetScale else secondStageScale }
val fadeIn = firstTransition.animateFloat(
transitionSpec = { tween(durationMillis = 400) },
label = "Fade in on start",
) { value -> if (value) 1f else 0f }
if (firstScaleStep.value == secondStageScale) {
isSecondStepLaunched.value = true
}
val modifier = if (!isSecondStepLaunched.value) {
Modifier.scale(firstScaleStep.value)
} else {
Modifier.scale(secondScaleStep.value)
}.alpha(fadeIn.value)
content(modifier)
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
}

View file

@ -1,266 +0,0 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.wallet.R
import kotlin.math.max
@Composable
internal fun StoriesScreen(
homeState: MutableState<HomeState>,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit,
) {
val state = homeState.value
var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
}
val goToNextStory = remember(currentStory, currentStoryIndex) {
{
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
state.stories[currentStoryIndex + 1]
} else {
state.firstStory
}
}
}
// todo refactor [REDACTED_TASK_KEY]
StoriesScreenContent(
modifier = Modifier
.fillMaxSize()
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentConfig(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,
currentStory = currentStory,
isScanInProgress = homeState.value.scanInProgress,
onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory,
onSearchTokensClick = onSearchTokensClick,
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick,
),
)
}
@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {
var isPressed by remember { mutableStateOf(value = false) }
val isPaused = isPressed || config.isScanInProgress
val currentStoryDuration = config.currentStory.duration
Box(
modifier = modifier.background(Color(0xFF010101)),
) {
Row(
modifier = Modifier.fillMaxSize(),
) {
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToPreviousStory()
isPressed = false
},
)
},
)
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToNextStory()
isPressed = false
},
)
},
)
}
Column(
modifier = Modifier
.statusBarsPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
StoriesProgressBar(
steps = config.storiesSize,
currentStep = config.currentStoryIndex,
stepDuration = currentStoryDuration,
paused = isPaused,
onStepFinish = config.onGoToNextStory,
)
Image(
painter = painterResource(id = R.drawable.ic_tangem_logo),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
)
.height(TangemTheme.dimens.size18)
.align(Alignment.Start),
)
when (config.currentStory) {
Stories.TangemIntro -> FirstStoriesContent(
isPaused = isPaused,
duration = currentStoryDuration,
)
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
isPaused = isPaused,
stepDuration = currentStoryDuration,
)
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
}
}
Column(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AnimatedVisibility(
visible = config.currentStory == Stories.Currencies,
enter = fadeIn(),
exit = fadeOut(),
) {
SearchCurrenciesButton(
modifier = Modifier.fillMaxWidth(),
onClick = config.onSearchTokensClick,
)
}
HomeButtons(
modifier = Modifier.fillMaxWidth(),
btnScanStateInProgress = config.isScanInProgress,
onScanButtonClick = config.onScanButtonClick,
onShopButtonClick = config.onShopButtonClick,
)
}
}
}
private data class StoriesScreenContentConfig(
val storiesSize: Int,
val currentStoryIndex: Int,
val currentStory: Stories,
val isScanInProgress: Boolean,
val onGoToPreviousStory: () -> Unit = {},
val onGoToNextStory: () -> Unit = {},
val onSearchTokensClick: () -> Unit = {},
val onScanButtonClick: () -> Unit = {},
val onShopButtonClick: () -> Unit = {},
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun StoriesScreenContentPreview(
@PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig,
) {
TangemThemePreview {
StoriesScreenContent(config = config)
}
}
private class StoriesScreenContentConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentConfig>(
collection = listOf(
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 0,
currentStory = Stories.TangemIntro,
isScanInProgress = true,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 1,
currentStory = Stories.RevolutionaryWallet,
isScanInProgress = false,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 2,
currentStory = Stories.UltraSecureBackup,
isScanInProgress = false,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 3,
currentStory = Stories.Currencies,
isScanInProgress = false,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 4,
currentStory = Stories.Web3,
isScanInProgress = false,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 5,
currentStory = Stories.WalletForEveryone,
isScanInProgress = false,
),
),
)
// endregion Preview

View file

@ -1,252 +0,0 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtonsV2
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.wallet.R
import kotlin.math.max
@Composable
internal fun StoriesScreenV2(
homeState: MutableState<HomeState>,
onCreateNewWalletButtonClick: () -> Unit,
onAddExistingWalletButtonClick: () -> Unit,
onScanButtonClick: () -> Unit,
) {
val state = homeState.value
var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
}
val goToNextStory = remember(currentStory, currentStoryIndex) {
{
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
state.stories[currentStoryIndex + 1]
} else {
state.firstStory
}
}
}
// todo refactor [REDACTED_TASK_KEY]
StoriesScreenContentV2(
modifier = Modifier
.fillMaxSize()
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentV2Config(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,
currentStory = currentStory,
isScanInProgress = homeState.value.scanInProgress,
onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory,
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,
onAddExistingWalletButtonClick = onAddExistingWalletButtonClick,
onScanButtonClick = onScanButtonClick,
),
)
}
@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) {
var isPressed by remember { mutableStateOf(value = false) }
val isPaused = isPressed || config.isScanInProgress
val currentStoryDuration = config.currentStory.duration
Box(
modifier = modifier.background(Color(0xFF010101)),
) {
Row(
modifier = Modifier.fillMaxSize(),
) {
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToPreviousStory()
isPressed = false
},
)
},
)
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToNextStory()
isPressed = false
},
)
},
)
}
Column(
modifier = Modifier
.statusBarsPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
StoriesProgressBar(
steps = config.storiesSize,
currentStep = config.currentStoryIndex,
stepDuration = currentStoryDuration,
paused = isPaused,
onStepFinish = config.onGoToNextStory,
)
Image(
painter = painterResource(id = R.drawable.ic_tangem_logo),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
)
.height(TangemTheme.dimens.size18)
.align(Alignment.Start),
)
when (config.currentStory) {
Stories.TangemIntro -> FirstStoriesContent(
isPaused = isPaused,
duration = currentStoryDuration,
)
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
isPaused = isPaused,
stepDuration = currentStoryDuration,
)
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
}
}
Column(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
HomeButtonsV2(
modifier = Modifier.fillMaxWidth(),
btnScanStateInProgress = config.isScanInProgress,
onScanButtonClick = config.onScanButtonClick,
onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick,
onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick,
)
}
}
}
private data class StoriesScreenContentV2Config(
val storiesSize: Int,
val currentStoryIndex: Int,
val currentStory: Stories,
val isScanInProgress: Boolean,
val onGoToPreviousStory: () -> Unit = {},
val onGoToNextStory: () -> Unit = {},
val onCreateNewWalletButtonClick: () -> Unit = {},
val onAddExistingWalletButtonClick: () -> Unit = {},
val onScanButtonClick: () -> Unit = {},
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun StoriesScreenContentV2Preview(
@PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config,
) {
TangemThemePreview {
StoriesScreenContentV2(config = config)
}
}
private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentV2Config>(
collection = listOf(
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 0,
currentStory = Stories.TangemIntro,
isScanInProgress = true,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 1,
currentStory = Stories.RevolutionaryWallet,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 2,
currentStory = Stories.UltraSecureBackup,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 3,
currentStory = Stories.Currencies,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 4,
currentStory = Stories.Web3,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 5,
currentStory = Stories.WalletForEveryone,
isScanInProgress = false,
),
),
)
// endregion Preview

View file

@ -1,229 +0,0 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun StoriesRevolutionaryWallet() {
SplitContent(
topContent = {
TopContent(
titleText = stringResourceSafe(id = R.string.story_awe_title),
subtitleText = stringResourceSafe(id = R.string.story_awe_description),
)
},
bottomContent = {
SpacerH32()
StoriesImage(
modifier = Modifier,
drawableResId = R.drawable.img_revolutionary_wallet,
)
},
)
}
@Composable
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResourceSafe(id = R.string.story_backup_title),
subtitleText = stringResourceSafe(id = R.string.story_backup_description),
)
},
bottomContent = {
SpacerH32()
FloatingCardsContent(
isPaused = isPaused,
stepDuration = stepDuration,
)
},
)
}
@Composable
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResourceSafe(id = R.string.story_currencies_title),
subtitleText = stringResourceSafe(id = R.string.story_currencies_description),
)
},
bottomContent = {
SpacerH32()
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
},
)
}
@Composable
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResourceSafe(id = R.string.story_web3_title),
subtitleText = stringResourceSafe(id = R.string.story_web3_description),
)
},
bottomContent = {
SpacerH(TangemTheme.dimens.spacing70)
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
},
)
}
@Composable
fun StoriesWalletForEveryone(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResourceSafe(id = R.string.story_finish_title),
subtitleText = stringResourceSafe(id = R.string.story_finish_description),
)
},
bottomContent = {
SpacerH32()
BoxWithGradient {
StoriesBottomImageAnimation(
initialScale = 2.6f,
secondStageScale = 1.2f,
targetScale = 1.1f,
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.img_tangem_for_everyone,
)
}
}
},
)
}
@Composable
private fun SplitContent(topContent: @Composable () -> Unit, bottomContent: @Composable () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
topContent()
bottomContent()
}
}
@Composable
private fun TopContent(titleText: String, subtitleText: String) {
SpacerH(TangemTheme.dimens.spacing36)
StoriesTitleText(
text = titleText,
)
SpacerH16()
StoriesSubtitleText(
subtitleText = subtitleText,
)
}
@Suppress("MagicNumber")
@Composable
private fun StoriesTitleText(text: String) {
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
text = text,
style = TangemTheme.typography.head,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
}
@Suppress("MagicNumber")
@Composable
private fun StoriesSubtitleText(subtitleText: String) {
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 400,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
text = subtitleText,
style = TangemTheme.typography.body1,
color = TangemColorPalette.Dark1,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = Modifier) {
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = ContentScale.Inside,
modifier = modifier.fillMaxSize(),
)
}
@Preview
@Composable
private fun RevolutionaryWalletPreview() {
StoriesRevolutionaryWallet()
}
@Preview
@Composable
private fun UltraSecureBackupPreview() {
StoriesUltraSecureBackup(
false,
6000,
)
}
@Preview
@Composable
private fun CurrenciesPreview() {
StoriesCurrencies(false, 6000)
}
@Preview
@Composable
private fun Web3Preview() {
StoriesWeb3(false, 6000)
}
@Preview
@Composable
private fun WalletForEveryonePreview() {
StoriesWalletForEveryone(6000)
}

View file

@ -1,141 +0,0 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.extensions.dpSize
import com.tangem.tap.common.compose.extensions.halfHeight
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.common.extensions.isEven
import com.tangem.tap.features.home.compose.HorizontalSlidingImage
import com.tangem.wallet.R
@Composable
fun StoriesCurrenciesContent(paused: Boolean, duration: Int) {
val currencyDrawableList = remember {
listOf(
R.drawable.currency0,
R.drawable.currency1,
R.drawable.currency2,
R.drawable.currency3,
R.drawable.currency4,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / currencyDrawableList.size }
val designItemHeight = remember { 82.dp }
BoxWithGradient {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
currencyDrawableList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 50.dp - 50.dp * index * decreaseRate
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Currency row",
)
SpacerH12()
}
}
}
}
@Suppress("MagicNumber")
@Composable
fun StoriesWeb3Content(paused: Boolean, duration: Int) {
val dappsItemList = remember {
listOf(
R.drawable.dapps1,
R.drawable.dapps1,
R.drawable.dapps2,
R.drawable.dapps3,
R.drawable.dapps4,
R.drawable.dapps5,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / dappsItemList.size }
val designItemHeight = 75.dp
BoxWithGradient {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
dappsItemList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 70.dp - 70.dp * index * decreaseRate
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Web3 row",
)
}
}
}
}
@Composable
internal fun BoxWithGradient(content: @Composable () -> Unit) {
val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current)
Box(modifier = Modifier.fillMaxSize()) {
content()
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(TangemTheme.dimens.size164 + bottomInsetsPx.dp)
.background(BottomGradient),
)
}
}
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
val scaleRate = itemSize.height / designItemHeight
return itemSize / scaleRate
}
private val BottomGradient: Brush = Brush.verticalGradient(
colors = listOf(
TangemColorPalette.Black.copy(alpha = 0f),
TangemColorPalette.Black.copy(alpha = 0.75f),
TangemColorPalette.Black.copy(alpha = 0.95f),
TangemColorPalette.Black,
),
)

View file

@ -1,93 +0,0 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
@Composable
fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
val progress = remember { Animatable(0f) }
LaunchedEffect(isPaused) {
if (isPaused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing,
),
)
}
}
val style = TextStyle(
fontSize = 46.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(TangemTheme.dimens.spacing94)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier,
text = stringResourceSafe(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
SpacerH(TangemTheme.dimens.spacing46)
Image(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.img_meet_tangem),
contentScale = ContentScale.Inside,
contentDescription = "Tangem Wallet card",
)
}
}
@Preview
@Composable
private fun FirstStoriesPreview() {
FirstStoriesContent(
false,
8000,
)
}

View file

@ -1,97 +0,0 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.asImageBitmap
import com.tangem.tap.common.compose.extensions.toAnimatable
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int) {
val imageBitmap = asImageBitmap(R.drawable.img_card_placeholder_wallet_2)
val cards = listOf(
FloatingCard.first(),
FloatingCard.second(),
FloatingCard.third(),
)
Box(modifier = Modifier.fillMaxSize()) {
cards.forEach { floatingCard ->
FloatingCard.Item(
isPaused = isPaused,
imageBitmap = imageBitmap,
cardValues = floatingCard,
stepDuration = stepDuration,
)
}
}
}
private data class CardValues(
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
val scale: AnimatedValue = AnimatedValue(1f, 1f),
)
private object FloatingCard {
@Suppress("TopLevelComposableFunctions")
@Composable
fun Item(isPaused: Boolean, stepDuration: Int, imageBitmap: ImageBitmap, cardValues: CardValues) {
Image(
bitmap = imageBitmap,
contentDescription = "Floating Tangem card",
modifier = Modifier
.graphicsLayer(
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
),
)
}
@Suppress("MagicNumber")
fun first(): CardValues = CardValues(
translateX = -400f to -350f,
translateY = 30f to 32f,
rotationX = 10f to 15f,
rotationY = 15f to 15f,
rotationZ = 40f to 27f,
scale = 0.6f to 0.6f,
)
@Suppress("MagicNumber")
fun second(): CardValues = CardValues(
translateX = 350f to 300f,
translateY = -70f to 0f,
rotationX = 30f to 48f,
rotationY = 0f to 5f,
rotationZ = -34f to -42f,
scale = 0.47f to 0.35f,
)
@Suppress("MagicNumber")
fun third(): CardValues = CardValues(
translateX = 320f to 250f,
translateY = 500f to 500f,
rotationX = 0f to 3f,
rotationY = 10f to 10f,
rotationZ = -45f to -30f,
scale = 0.6f to 0.75f,
)
}

View file

@ -1,105 +0,0 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R
@Composable
internal fun HomeButtons(
btnScanStateInProgress: Boolean,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = modifier,
) {
ScanCardButton(
modifier = Modifier
.weight(weight = 1f)
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
SpacerW12()
OrderCardButton(
modifier = Modifier
.weight(weight = 1f)
.testTag(StoriesScreenTestTags.ORDER_BUTTON),
onClick = onShopButtonClick,
)
}
}
@Composable
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_scan),
useDarkerColors = false,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = onClick,
showProgress = showProgress,
)
}
@Composable
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_order),
useDarkerColors = true,
onClick = onClick,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) {
TangemThemePreview {
Box(
modifier = Modifier.background(Color.Black),
) {
HomeButtons(
btnScanStateInProgress = state.btnScanStateInProgress,
onScanButtonClick = {},
onShopButtonClick = {},
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
)
}
}
}
private class HomeButtonsParameterProvider : CollectionPreviewParameterProvider<HomeButtonsState>(
collection = listOf(
HomeButtonsState(
btnScanStateInProgress = false,
),
HomeButtonsState(
btnScanStateInProgress = true,
),
),
)
private data class HomeButtonsState(
val btnScanStateInProgress: Boolean,
)
// endregion Preview

View file

@ -1,124 +0,0 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R
@Composable
internal fun HomeButtonsV2(
btnScanStateInProgress: Boolean,
onScanButtonClick: () -> Unit,
onCreateNewWalletButtonClick: () -> Unit,
onAddExistingWalletButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
CreateNewWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(StoriesScreenTestTags.CREATE_NEW_WALLET_BUTTON),
onClick = onCreateNewWalletButtonClick,
)
AddExistingWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(StoriesScreenTestTags.ADD_EXISTING_WALLET_BUTTON),
onClick = onAddExistingWalletButtonClick,
)
ScanCardButton(
modifier = Modifier
.fillMaxWidth()
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
}
}
@Composable
private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_create_new_wallet),
useDarkerColors = false,
onClick = onClick,
)
}
@Composable
private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_add_existing_wallet),
useDarkerColors = true,
onClick = onClick,
)
}
@Composable
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_scan),
useDarkerColors = true,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = onClick,
showProgress = showProgress,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) {
TangemThemePreview {
Box(
modifier = Modifier.background(Color.Black),
) {
HomeButtonsV2(
btnScanStateInProgress = state.btnScanStateInProgress,
onCreateNewWalletButtonClick = {},
onAddExistingWalletButtonClick = {},
onScanButtonClick = {},
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
)
}
}
}
private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider<HomeButtonsV2State>(
collection = listOf(
HomeButtonsV2State(
btnScanStateInProgress = false,
),
HomeButtonsV2State(
btnScanStateInProgress = true,
),
),
)
private data class HomeButtonsV2State(
val btnScanStateInProgress: Boolean,
)
// endregion Preview

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Composable
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.common_search_tokens),
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
showProgress = false,
useDarkerColors = true,
onClick = onClick,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SearchCurrenciesButtonPreview() {
TangemThemePreview {
Box(
modifier = Modifier
.background(color = Color.Black)
.padding(all = TangemTheme.dimens.spacing16),
) {
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
}
}
}
// endregion Preview

View file

@ -1,51 +0,0 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.material3.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun StoriesButton(
text: String,
useDarkerColors: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
showProgress: Boolean = false,
) {
TangemButton(
modifier = modifier,
text = text,
icon = icon,
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
showProgress = showProgress,
enabled = true,
shape = TangemTheme.shapes.roundedCornersXMedium,
textStyle = TangemTheme.typography.subtitle1,
iconPadding = when (icon) {
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
is TangemButtonIconPosition.End,
is TangemButtonIconPosition.None,
-> TangemTheme.dimens.spacing8
},
onClick = onClick,
)
}
private val LighterButtonColors: ButtonColors = ButtonColors(
containerColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledContainerColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.Dark6,
)
private val DarkerButtonColors: ButtonColors = ButtonColors(
containerColor = TangemColorPalette.Dark4,
contentColor = TangemColorPalette.White,
disabledContainerColor = TangemColorPalette.Dark4,
disabledContentColor = TangemColorPalette.White,
)

View file

@ -1,112 +0,0 @@
package com.tangem.tap.features.home.compose.views
import android.provider.Settings
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
@Composable
fun StoriesProgressBar(
steps: Int,
currentStep: Int,
paused: Boolean = false,
stepDuration: Int = 8_000,
onStepFinish: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
val context = LocalContext.current
val animatorSpeed = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
LaunchedEffect(paused, currentStep, animatorSpeed) {
if (paused) {
progress.stop()
} else {
if (animatorSpeed == 0f) {
progress.snapTo(1f)
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
}
onStepFinish()
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
),
) {
for (index in 0..steps) {
Row(
modifier = Modifier
.height(TangemTheme.dimens.size2)
.weight(1f)
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(TangemColorPalette.White.copy(alpha = .2f)),
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(TangemColorPalette.White)
.fillMaxHeight()
.let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
else -> it
}
},
)
}
if (index != steps) {
SpacerW4()
}
}
}
}
@Preview
@Composable
private fun StoriesProgressBarPreview() {
Box(
modifier = Modifier
.wrapContentSize()
.background(TangemColorPalette.Black)
.padding(vertical = TangemTheme.dimens.spacing16),
) {
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.home.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.home.DefaultHomeComponent
import com.tangem.tap.features.home.HomeModel
import com.tangem.tap.features.home.api.HomeComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface HomeFeatureModule {
@Binds
fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory
@Binds
@IntoMap
@ClassKey(HomeModel::class)
fun bindModel(model: HomeModel): Model
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.features.home.errors
import com.tangem.common.core.TangemError
interface TangemSdkErrorHandler {
fun onErrorReceived(error: TangemError)
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.settings.usercountry.models.UserCountry
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class HomeAction : Action {
data object OnCreate : HomeAction()
/**
* Action for scanning card
*
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
*/
data class ReadCard(val scope: CoroutineScope) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data class UserCountryLoaded(val userCountry: UserCountry) : HomeAction()
}

View file

@ -1,147 +0,0 @@
package com.tangem.tap.features.home.redux
import android.content.res.Resources
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.Locale
internal const val HIDE_PROGRESS_DELAY = 400L
object HomeMiddleware {
val handler = homeMiddleware
private val SYSTEM_LANGUAGE =
runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" }
private val APP_LANGUAGE = Locale.getDefault().language
private val UTM_MARKS = "utm_source=tangem-app" +
"&utm_medium=app" +
"&utm_campaign=prospect-$SYSTEM_LANGUAGE" +
"&utm_content=devicelang-$APP_LANGUAGE"
val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?$UTM_MARKS"
}
private val homeMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
handleHomeAction(action)
next(action)
}
}
}
private fun handleHomeAction(action: Action) {
when (action) {
is HomeAction.OnCreate -> {
Analytics.eraseContext()
Analytics.send(IntroductionProcess.ScreenOpened())
store.dispatch(GlobalAction.RestoreAppCurrency)
}
is HomeAction.ReadCard -> {
action.scope.launch {
readCard()
}
}
}
}
private suspend fun readCard() {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onFailure = {
Timber.e(it, "Unable to scan card")
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build().guard {
Timber.e("User wallet not created")
return@launch
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
sendSignedInCardAnalyticsEvent(scanResponse)
store.onUserWalletSelected(userWallet = userWallet)
}
.doOnResult {
navigateTo(AppRoute.Wallet)
}
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
if (currency != null) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
Analytics.send(
event = Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = Basic.SignedIn.SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private suspend fun navigateTo(route: AppRoute) {
store.dispatchNavigationAction { push(route) }
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.tap.common.redux.AppState
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Action
object HomeReducer {
fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): HomeState {
if (action !is HomeAction) return appState.homeState
return when (action) {
is HomeAction.ScanInProgress -> {
appState.homeState.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.UserCountryLoaded -> {
val stories = if (action.userCountry.needApplyFCARestrictions()) {
getRestrictedStories()
} else {
Stories.entries
}
appState.homeState.copy(
stories = stories.toImmutableList(),
)
}
else -> appState.homeState
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.tap.features.home.redux
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.StateType
// todo refactor [REDACTED_TASK_KEY]
data class HomeState(
val scanInProgress: Boolean = false,
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
) : StateType {
val firstStory: Stories get() = stories[0]
fun stepOf(story: Stories): Int = stories.indexOf(story)
}
enum class Stories(val duration: Int = 6000) {
TangemIntro,
RevolutionaryWallet,
UltraSecureBackup,
Currencies,
Web3,
WalletForEveryone,
}
/**
* For FCA restriction stories
*/
fun getRestrictedStories(): List<Stories> {
return Stories.entries.filterNot { it == Stories.Currencies }
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.features.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import javax.inject.Inject
class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester {
override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password {
return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY]
}
}

View file

@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =
callSdk { changeAuth(unlockHotWallet, auth) }
override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId =
callSdk { removeBiometryAuthIfPresented(id) }
override suspend fun derivePublicKey(
unlockHotWallet: UnlockHotWallet,
request: DeriveWalletRequest,

View file

@ -4,21 +4,12 @@ import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
import com.tangem.common.routing.entity.InitScreenLaunchMode
/**
[REDACTED_AUTHOR]
*/
class BackgroundScanIntentHandler(
private val hasSavedUserWalletsProvider: () -> Boolean,
private val scope: CoroutineScope,
) : IntentHandler, AffectsNavigation {
class BackgroundScanIntentHandler {
private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED,
@ -26,8 +17,15 @@ class BackgroundScanIntentHandler(
NfcAdapter.ACTION_TAG_DISCOVERED,
)
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
if (isFromForeground) return true
fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode {
return if (shouldOpenScanCard(intent)) {
InitScreenLaunchMode.WithCardScan
} else {
InitScreenLaunchMode.Standard
}
}
private fun shouldOpenScanCard(intent: Intent?): Boolean {
if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -36,15 +34,9 @@ class BackgroundScanIntentHandler(
@Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
}
if (tag == null) return false
intent.action = null
if (hasSavedUserWalletsProvider.invoke()) {
store.dispatchOnMain(WelcomeAction.ProceedWithCard)
} else {
store.dispatchOnMain(HomeAction.ReadCard(scope = scope))
}
return true
return tag != null
}
}

View file

@ -4,8 +4,8 @@ import android.content.Intent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
import timber.log.Timber
import java.net.URLDecoder

View file

@ -24,8 +24,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
@ -212,15 +210,11 @@ internal class MainViewModel @Inject constructor(
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
val cardProvider: () -> ScanResponse? = {
userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
}
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
cardProvider = { cardProvider.invoke()?.card },
userWalletProvider = { userWalletsListManager.selectedUserWalletSync },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.welcome.component
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface WelcomeComponent : ComposableContentComponent {
data class Params(
val launchMode: InitScreenLaunchMode,
val intent: SerializableIntent?,
)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -44,10 +45,9 @@ internal class WelcomeModel @Inject constructor(
subscribeToStoreChanges()
initGlobalState()
val welcomeAction = if (params.intent != null) {
WelcomeAction.ProceedWithIntent(params.intent.toIntent())
} else {
WelcomeAction.ProceedWithBiometrics()
val welcomeAction = when (params.launchMode) {
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard
is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics(params.intent?.toIntent())
}
store.dispatch(welcomeAction)

View file

@ -11,19 +11,17 @@ import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -44,7 +42,7 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this)
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
afterUnlockIntent = action.afterUnlockIntent ?: state.intent,
)
@ -55,7 +53,7 @@ internal class WelcomeMiddleware {
}
}
private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) {
private suspend fun proceedWithIntent(initialIntent: Intent) {
Timber.d(
"""
Proceeding with intent
@ -63,15 +61,12 @@ internal class WelcomeMiddleware {
""".trimIndent(),
)
val handler = BackgroundScanIntentHandler(
scope = scope,
hasSavedUserWalletsProvider = { true },
)
val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false)
val hasUncompletedBackup = backupService.hasIncompletedBackup
if (!isBackgroundScanHandled && !hasUncompletedBackup) {
if (!hasUncompletedBackup) {
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent))
} else {
store.dispatchWithMain(WelcomeAction.ProceedWithCard)
}
}
@ -139,7 +134,7 @@ internal class WelcomeMiddleware {
}
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) {
// TODO [REDACTED_TASK_KEY]
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics
if (userWallet !is UserWallet.Cold) {
return

View file

@ -4,11 +4,16 @@ import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
internal class DefaultAuthProvider(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewListRepository: Boolean = false,
) : AuthProvider {
override fun getCardPublicKey(): String {
val userWallet = userWalletsListManager.selectedUserWalletSync
override suspend fun getCardPublicKey(): String {
val userWallet = getSelectedWallet()
if (userWallet !is UserWallet.Cold) {
return ""
@ -17,8 +22,8 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
return userWallet.scanResponse.card.cardPublicKey.toHexString()
}
override fun getCardId(): String {
val userWallet = userWalletsListManager.selectedUserWalletSync
override suspend fun getCardId(): String {
val userWallet = getSelectedWallet()
if (userWallet !is UserWallet.Cold) {
return ""
@ -27,9 +32,25 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
return userWallet.scanResponse.card.cardId
}
override fun getCardsPublicKeys(): Map<String, String> {
return userWalletsListManager.userWalletsSync.filterIsInstance<UserWallet.Cold>().associate {
override suspend fun getCardsPublicKeys(): Map<String, String> {
return getWallets().filterIsInstance<UserWallet.Cold>().associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
private suspend fun getWallets(): List<UserWallet> {
return if (useNewListRepository) {
userWalletsListRepository.userWalletsSync()
} else {
userWalletsListManager.userWalletsSync
}
}
private suspend fun getSelectedWallet(): UserWallet? {
return if (useNewListRepository) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultAppVersionProvider
@ -22,8 +24,16 @@ internal class AuthModule {
@Provides
@Singleton
fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider {
return DefaultAuthProvider(userWalletsListManager)
fun provideAuthProvider(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): AuthProvider {
return DefaultAuthProvider(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides

View file

@ -13,9 +13,9 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
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.transaction.models.AssetRequirementsCondition

View file

@ -12,7 +12,7 @@ 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.currency.CryptoCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
@ -27,7 +27,7 @@ class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val logEnabled: Boolean,
private val cardProvider: () -> CardDTO?,
private val userWalletProvider: () -> UserWallet?,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
@ -103,8 +103,8 @@ class MoonPayService(
}
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
val checkCardExchange = !card.isStart2Coin
val userWallet = userWalletProvider() ?: return false
val checkCardExchange = userWallet !is UserWallet.Cold || !userWallet.scanResponse.card.isStart2Coin
if (!checkCardExchange) return false

View file

@ -158,4 +158,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
ZkLinkNova, ZkLinkNovaTestnet -> null
KaspaTestnet -> null
Pepecoin, PepecoinTestnet -> null
Hyperliquid, HyperliquidTestnet -> null
}

View file

@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -77,4 +80,7 @@ data class DaggerGraphState(
val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
val userWalletsListRepository: UserWalletsListRepository? = null,
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
val tangemHotSdk: TangemHotSdk? = null,
) : StateType

View file

@ -8,12 +8,21 @@ import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -31,6 +40,7 @@ import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -42,12 +52,12 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent
import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent
@ActivityScoped
@Suppress("LongParameterList", "LargeClass")
@ -66,6 +76,7 @@ internal class ChildFactory @Inject constructor(
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val stakingComponentFactory: StakingComponent.Factory,
private val swapComponentFactory: SwapComponent.Factory,
@ -84,16 +95,25 @@ internal class ChildFactory @Inject constructor(
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory,
private val accountDetailsComponentFactory: AccountDetailsComponent.Factory,
private val archivedAccountListComponentFactory: ArchivedAccountListComponent.Factory,
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val usedeskComponentFactory: UsedeskComponent.Factory,
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val walletActivationComponentFactory: WalletActivationComponent.Factory,
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -130,13 +150,22 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Welcome -> {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
createComponentChild(
context = context,
params = Unit,
componentFactory = newWelcomeComponentFactory,
)
} else {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
}
}
is AppRoute.WalletSettings -> {
createComponentChild(
@ -290,7 +319,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Home -> {
createComponentChild(
context = context,
params = Unit,
params = HomeComponent.Params(route.launchMode),
componentFactory = homeComponentFactory,
)
}
@ -385,6 +414,7 @@ internal class ChildFactory @Inject constructor(
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
source = route.source,
nextRoute = AppRoute.Home(),
),
componentFactory = pushNotificationsComponentFactory,
)
@ -438,6 +468,8 @@ internal class ChildFactory @Inject constructor(
initialCurrency = route.initialCurrency,
selectedCurrency = route.selectedCurrency,
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
showSendViaSwapNotification = route.showSendViaSwapNotification,
analyticsCategoryName = route.analyticsCategoryName,
),
componentFactory = chooseManagedTokensComponentFactory,
)
@ -456,6 +488,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = createMobileWalletComponentFactory,
)
}
is AppRoute.UpgradeWallet -> {
createComponentChild(
context = context,
params = UpgradeWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = upgradeWalletComponentFactory,
)
}
is AppRoute.AddExistingWallet -> {
createComponentChild(
context = context,
@ -463,6 +504,33 @@ internal class ChildFactory @Inject constructor(
componentFactory = addExistingWalletComponentFactory,
)
}
is AppRoute.WalletActivation -> {
createComponentChild(
context = context,
params = WalletActivationComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletActivationComponentFactory,
)
}
is AppRoute.CreateWalletBackup -> {
createComponentChild(
context = context,
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = createWalletBackupComponentFactory,
)
}
is AppRoute.UpdateAccessCode -> {
createComponentChild(
context = context,
params = UpdateAccessCodeComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = updateAccessCodeComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,
@ -483,6 +551,49 @@ internal class ChildFactory @Inject constructor(
componentFactory = sendWithSwapComponentFactory,
)
}
is AppRoute.CreateAccount -> {
createComponentChild(
context = context,
params = AccountCreateEditComponent.Params.Create(
userWalletId = route.userWalletId,
),
componentFactory = accountCreateEditComponentFactory,
)
}
is AppRoute.EditAccount -> {
createComponentChild(
context = context,
params = AccountCreateEditComponent.Params.Edit(
account = route.account,
),
componentFactory = accountCreateEditComponentFactory,
)
}
is AppRoute.AccountDetails -> {
createComponentChild(
context = context,
params = AccountDetailsComponent.Params(
account = route.account,
),
componentFactory = accountDetailsComponentFactory,
)
}
is AppRoute.ArchivedAccountList -> {
createComponentChild(
context = context,
params = ArchivedAccountListComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = archivedAccountListComponentFactory,
)
}
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(),
componentFactory = tangemPayDetailsComponentFactory,
)
}
}
}
}