Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-30 17:56:53 +05:00
parent 78720c5cab
commit fb54de419b
98 changed files with 750 additions and 787 deletions

View file

@ -232,6 +232,8 @@ dependencies {
implementation(projects.features.welcome.impl) implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.api)
implementation(projects.features.createWalletSelection.impl) implementation(projects.features.createWalletSelection.impl)
implementation(projects.features.home.api)
implementation(projects.features.home.impl)
/** AndroidX libraries */ /** AndroidX libraries */
implementation(deps.androidx.core.ktx) implementation(deps.androidx.core.ktx)

View file

@ -3,8 +3,8 @@ package com.tangem.tests
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDisclaimerScreen import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.onStoriesScreen import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.intent.KIntent import io.github.kakaocup.kakao.intent.KIntent

View file

@ -176,13 +176,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher 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
internal val viewModel: MainViewModel by viewModels() internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode> private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private val dialogManager = DialogManager() private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>() private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -344,12 +353,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
private fun initIntentHandlers() { private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(onPushClickedIntentHandler)
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler()) intentProcessor.addHandler(walletConnectLinkIntentHandler)
} }
} }
@ -435,9 +442,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} }
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
} }
intentProcessor.handleIntent( intentProcessor.handleIntent(
intent = intentWhichStartedActivity, intent = intentWhichStartedActivity,
@ -452,7 +465,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val route = when { val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false) shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)
shouldShowInitialPush -> AppRoute.PushNotification shouldShowInitialPush -> AppRoute.PushNotification
else -> AppRoute.Home else -> AppRoute.Home(launchMode = launchMode)
} }
store.dispatchNavigationAction { replaceAll(route) } store.dispatchNavigationAction { replaceAll(route) }

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

@ -6,9 +6,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam 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.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState

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.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

@ -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.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer 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.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState( return AppState(
globalState = globalReducer(action, state), globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state), 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.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState 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.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -20,7 +18,6 @@ import org.rekotlin.StateType
data class AppState( data class AppState(
val globalState: GlobalState = GlobalState(), val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val detailsState: DetailsState = DetailsState(), val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(), val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(), val welcomeState: WelcomeState = WelcomeState(),
@ -32,7 +29,6 @@ data class AppState(
return listOf( return listOf(
logMiddleware, logMiddleware,
GlobalMiddleware.handler, GlobalMiddleware.handler,
HomeMiddleware.handler,
DetailsMiddleware().detailsMiddleware, DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware, WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware, BackupMiddleware().backupMiddleware,

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam 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.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject 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.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store 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_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 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 { fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
@ -62,8 +62,8 @@ internal object ScanFailsDialog {
source = sourceAnalytics, source = sourceAnalytics,
), ),
) )
val locale = LocaleRegionProvider().getRegion() val locale = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link) store.dispatchOpenUrl(link)
} }
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {

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

@ -233,7 +233,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes() deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home) } store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit) return CompletionResult.Success(Unit)
} }

View file

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

@ -4,21 +4,12 @@ import android.content.Intent
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.Tag import android.nfc.Tag
import android.os.Build import android.os.Build
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.common.routing.entity.InitScreenLaunchMode
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
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class BackgroundScanIntentHandler( class BackgroundScanIntentHandler {
private val hasSavedUserWalletsProvider: () -> Boolean,
private val scope: CoroutineScope,
) : IntentHandler, AffectsNavigation {
private val nfcActions = arrayOf( private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED, NfcAdapter.ACTION_NDEF_DISCOVERED,
@ -26,8 +17,15 @@ class BackgroundScanIntentHandler(
NfcAdapter.ACTION_TAG_DISCOVERED, NfcAdapter.ACTION_TAG_DISCOVERED,
) )
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode {
if (isFromForeground) return true return if (shouldOpenScanCard(intent)) {
InitScreenLaunchMode.WithCardScan
} else {
InitScreenLaunchMode.Standard
}
}
private fun shouldOpenScanCard(intent: Intent?): Boolean {
if (intent == null || intent.action !in nfcActions) return false if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -36,15 +34,9 @@ class BackgroundScanIntentHandler(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
} }
if (tag == null) return false
intent.action = null 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.dispatchOnMain
import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction 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.AffectsNavigation
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store import com.tangem.tap.store
import timber.log.Timber import timber.log.Timber
import java.net.URLDecoder import java.net.URLDecoder

View file

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

View file

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

View file

@ -20,10 +20,8 @@ import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.* import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState 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.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.Middleware import org.rekotlin.Middleware
import timber.log.Timber import timber.log.Timber
@ -44,7 +42,7 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) { private fun handleAction(action: WelcomeAction, state: WelcomeState) {
mainScope.launch { mainScope.launch {
when (action) { when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this) is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
afterUnlockIntent = action.afterUnlockIntent ?: state.intent, 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( Timber.d(
""" """
Proceeding with intent Proceeding with intent
@ -63,15 +61,12 @@ internal class WelcomeMiddleware {
""".trimIndent(), """.trimIndent(),
) )
val handler = BackgroundScanIntentHandler(
scope = scope,
hasSavedUserWalletsProvider = { true },
)
val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false)
val hasUncompletedBackup = backupService.hasIncompletedBackup val hasUncompletedBackup = backupService.hasIncompletedBackup
if (!isBackgroundScanHandled && !hasUncompletedBackup) { if (!hasUncompletedBackup) {
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent)) store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent))
} else {
store.dispatchWithMain(WelcomeAction.ProceedWithCard)
} }
} }

View file

@ -40,7 +40,7 @@ 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.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent 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.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.home.api.HomeComponent import com.tangem.features.home.api.HomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.component.RoutingComponent.Child
import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.android.scopes.ActivityScoped
@ -131,6 +131,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = WelcomeComponent.Params( params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent, intent = route.intent,
), ),
componentFactory = welcomeComponentFactory, componentFactory = welcomeComponentFactory,
@ -288,7 +289,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Home -> { is AppRoute.Home -> {
createComponentChild( createComponentChild(
context = context, context = context,
params = Unit, params = HomeComponent.Params(route.launchMode),
componentFactory = homeComponentFactory, componentFactory = homeComponentFactory,
) )
} }
@ -380,7 +381,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.PushNotification -> { is AppRoute.PushNotification -> {
createComponentChild( createComponentChild(
context = context, context = context,
params = PushNotificationsComponent.Params.Route(AppRoute.Home), params = PushNotificationsComponent.Params.Route(AppRoute.Home()),
componentFactory = pushNotificationsComponentFactory, componentFactory = pushNotificationsComponentFactory,
) )
} }

View file

@ -3,6 +3,7 @@ package com.tangem.common.routing
import android.os.Bundle import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.common.routing.entity.SerializableIntent import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
@ -22,10 +23,14 @@ sealed class AppRoute(val path: String) : Route {
data object Initial : AppRoute(path = "/initial") data object Initial : AppRoute(path = "/initial")
@Serializable @Serializable
data object Home : AppRoute(path = "/home") data class Home(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
) : AppRoute(path = "/home")
@Serializable @Serializable
data class Welcome( data class Welcome(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
// we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware
val intent: SerializableIntent? = null, val intent: SerializableIntent? = null,
) : AppRoute(path = "/welcome"), RouteBundleParams { ) : AppRoute(path = "/welcome"), RouteBundleParams {

View file

@ -0,0 +1,13 @@
package com.tangem.common.routing.entity
import kotlinx.serialization.Serializable
@Serializable
sealed class InitScreenLaunchMode {
@Serializable
data object Standard : InitScreenLaunchMode()
@Serializable
data object WithCardScan : InitScreenLaunchMode()
}

View file

@ -14,6 +14,7 @@ data class SerializableIntent(
val packageValue: String?, val packageValue: String?,
val component: String?, val component: String?,
val flags: Int, val flags: Int,
// CAUTION: works wrong with SerializableBundle constructor(bundle: Bundle), need to be removed
val extras: SerializableBundle?, val extras: SerializableBundle?,
) { ) {

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.compose.extensions package com.tangem.core.ui.utils
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector1D

View file

@ -1,9 +1,15 @@
package com.tangem.core.ui.utils package com.tangem.core.ui.utils
import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@Stable @Stable
@Composable @Composable
@ -15,4 +21,16 @@ fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.curr
fun Dp.toPx(density: Float): Float = this.value * density fun Dp.toPx(density: Float): Float = this.value * density
fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density)
fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density
fun Context.pxToDp(px: Float): Float = (px / resources.displayMetrics.density).roundToInt().toFloat()
@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

@ -1,4 +1,4 @@
package com.tangem.tap.common.compose.extensions package com.tangem.core.ui.utils
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources

View file

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 509 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 484 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 901 KiB

After

Width:  |  Height:  |  Size: 901 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 818 KiB

After

Width:  |  Height:  |  Size: 818 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Before After
Before After

View file

@ -1,6 +0,0 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
fun Int.isEven(): Boolean = this % 2 == 0

View file

@ -51,7 +51,7 @@ internal class DisclaimerModel @Inject constructor(
} else { } else {
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
neverRequestPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION)
router.replaceAll(AppRoute.Home) router.replaceAll(AppRoute.Home())
} }
} }
} }

1
features/home/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,17 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.home.api"
}
dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Common */
implementation(projects.common.routing)
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.home.api
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface HomeComponent : ComposableContentComponent {
data class Params(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
)
interface Factory : ComponentFactory<Params, HomeComponent> {
override fun create(context: AppComponentContext, params: Params): HomeComponent
}
}

1
features/home/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,69 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.home.impl"
}
dependencies {
/** Api */
implementation(projects.features.home.api)
implementation(projects.features.hotWallet.api)
/** Core modules */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.res)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
/** Common */
implementation(projects.common.routing)
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.core)
implementation(projects.domain.card)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
/** AndroidX libraries */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.runtime.ktx)
/** Compose libraries */
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.coil)
implementation(deps.decompose.ext.compose)
/** Firebase */
implementation(deps.firebase.analytics)
/** Tangem libraries */
implementation(tangemDeps.card.android)
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.home.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.model.HomeModel
import com.tangem.features.home.impl.ui.Home
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultHomeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: HomeComponent.Params,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : HomeComponent, AppComponentContext by appComponentContext {
private val model: HomeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
Home(
state = state,
modifier = modifier,
isV2StoriesEnabled = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@AssistedFactory
interface Factory : HomeComponent.Factory {
override fun create(context: AppComponentContext, params: HomeComponent.Params): DefaultHomeComponent
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.home.impl.analytics
internal sealed class AnalyticsParam {
sealed class CurrencyType(val value: String) {
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.home.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class IntroductionProcess(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Introduction Process", event, params) {
object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
object ButtonTokensList : IntroductionProcess("Button - Tokens List")
object ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
object ButtonScanCard : IntroductionProcess("Button - Scan Card")
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.home.impl.analytics
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.card.CardTypesResolver
import com.tangem.utils.converter.Converter
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
internal 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

@ -0,0 +1,11 @@
package com.tangem.features.home.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Shop(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {
object ScreenOpened : Shop("Shop Screen Opened")
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.home.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.DefaultHomeComponent
import com.tangem.features.home.impl.model.HomeModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindComponent(factory: DefaultHomeComponent.Factory): HomeComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(HomeModel::class)
fun provideModel(model: HomeModel): Model
}

View file

@ -0,0 +1,268 @@
package com.tangem.features.home.impl.model
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.SignedIn
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
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.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter
import com.tangem.features.home.impl.analytics.Shop
import com.tangem.features.home.impl.ui.state.HomeUM
import com.tangem.features.home.impl.ui.state.Stories
import com.tangem.features.home.impl.ui.state.getRestrictedStories
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@Suppress("LongParameterList")
@ModelScoped
internal class HomeModel @Inject constructor(
paramsContainer: ParamsContainer,
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,
private val selectWalletUseCase: SelectWalletUseCase,
private val appRouter: AppRouter,
private val getUserCountryUseCase: GetUserCountryUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
val params = paramsContainer.require<HomeComponent.Params>()
private val _uiState = MutableStateFlow(
HomeUM(
scanInProgress = false,
stories = getRestrictedStories().toImmutableList(),
onScanClick = ::onScanClick,
onShopClick = ::onShopClick,
onSearchTokensClick = ::onSearchTokensClick,
onCreateNewWalletClick = ::onCreateNewWalletClick,
onAddExistingWalletClick = ::onAddExistingWalletClick,
),
)
val uiState = _uiState.asStateFlow()
init {
analyticsEventHandler.send(IntroductionProcess.ScreenOpened)
observeUserCountryChanges()
when (params.launchMode) {
InitScreenLaunchMode.Standard -> Unit
InitScreenLaunchMode.WithCardScan -> scanCard()
}
}
private fun observeUserCountryChanges() {
getUserCountryUseCase.invoke()
.distinctUntilChanged()
.filterNotNull()
.onEach { result ->
val userCountry = result.getOrNull() ?: UserCountry.Other(Locale.getDefault().country)
updateStoriesForCountry(userCountry)
}
.flowOn(dispatchers.io)
.launchIn(modelScope)
}
private fun updateStoriesForCountry(userCountry: UserCountry) {
val stories = if (userCountry.needApplyFCARestrictions()) {
getRestrictedStories()
} else {
Stories.entries
}
_uiState.update {
it.copy(stories = stories.toImmutableList())
}
}
private fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
scanCard()
}
private 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) }
}
private fun onSearchTokensClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList)
router.push(AppRoute.ManageTokens(Source.STORIES))
}
private fun onCreateNewWalletClick() {
router.push(AppRoute.CreateWalletSelection)
}
private fun onAddExistingWalletClick() {
router.push(AppRoute.AddExistingWallet)
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
val analyticsSource = AnalyticsParam.ScreensSources.Intro
scanCardProcessor.scan(
analyticsSource = analyticsSource,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
} else {
setLoading(true)
}
},
onFailure = { error ->
handleScanError(error)
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
setLoading(false)
return
}
saveWalletUseCase(userWallet).fold(
ifLeft = {
Timber.e(it.toString(), "Unable to save user wallet")
setLoading(false)
},
ifRight = {
sendSignedInCardAnalyticsEvent(scanResponse)
// Select the wallet using new mechanism
selectWalletUseCase(userWallet.walletId).fold(
ifLeft = {
Timber.e("Unable to select user wallet: $it")
setLoading(false)
},
ifRight = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
appRouter.replaceAll(AppRoute.Wallet)
},
)
},
)
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = "1",
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private fun setLoading(isLoading: Boolean) {
_uiState.update { it.copy(scanInProgress = isLoading) }
}
fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> {
handleNfcFeatureUnavailable()
}
is TangemSdkError -> {
Timber.e(error, "Scan error occurred")
}
else -> {
Timber.e(error, "Error happened")
}
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
}
companion object {
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.home.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
import com.tangem.features.home.impl.ui.compose.StoriesScreen
import com.tangem.features.home.impl.ui.compose.StoriesScreenV2
import com.tangem.features.home.impl.ui.state.HomeUM
@Composable
internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier = Modifier) {
SystemBarsIconsDisposable(darkIcons = false)
if (isV2StoriesEnabled) {
StoriesScreenV2(
modifier = modifier,
state = state,
onCreateNewWalletButtonClick = state.onCreateNewWalletClick,
onAddExistingWalletButtonClick = state.onAddExistingWalletClick,
onScanButtonClick = state.onScanClick,
)
} else {
StoriesScreen(
modifier = modifier,
state = state,
onScanButtonClick = state.onScanClick,
onShopButtonClick = state.onShopClick,
onSearchTokensClick = state.onSearchTokensClick,
)
}
ChangeRootBackgroundColorEffect(TangemColorPalette.Black)
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose package com.tangem.features.home.impl.ui.compose
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -19,8 +19,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.extensions.AnimatedValue import com.tangem.core.ui.utils.AnimatedValue
import com.tangem.tap.common.compose.extensions.toAnimatable import com.tangem.core.ui.utils.toAnimatable
private const val SCALE_SWITCH_BARRIER = 1.15f private const val SCALE_SWITCH_BARRIER = 1.15f

View file

@ -1,6 +1,6 @@
@file:Suppress("MagicNumber") @file:Suppress("MagicNumber")
package com.tangem.tap.features.home.compose package com.tangem.features.home.impl.ui.compose
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -23,24 +23,23 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.* import com.tangem.features.home.impl.ui.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons import com.tangem.features.home.impl.ui.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton import com.tangem.features.home.impl.ui.compose.views.SearchCurrenciesButton
import com.tangem.tap.features.home.compose.views.StoriesProgressBar import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState import com.tangem.features.home.impl.ui.state.Stories
import com.tangem.tap.features.home.redux.Stories import com.tangem.core.ui.R
import com.tangem.wallet.R import com.tangem.features.home.impl.ui.state.HomeUM
import kotlin.math.max import kotlin.math.max
@Composable @Composable
internal fun StoriesScreen( internal fun StoriesScreen(
homeState: MutableState<HomeState>, state: HomeUM,
onScanButtonClick: () -> Unit, onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit, onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit, onSearchTokensClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
val state = homeState.value
var currentStory by remember { mutableStateOf(state.firstStory) } var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
@ -59,14 +58,14 @@ internal fun StoriesScreen(
// todo refactor [REDACTED_TASK_KEY] // todo refactor [REDACTED_TASK_KEY]
StoriesScreenContent( StoriesScreenContent(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER), .testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentConfig( config = StoriesScreenContentConfig(
storiesSize = state.stories.lastIndex, storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex, currentStoryIndex = currentStoryIndex,
currentStory = currentStory, currentStory = currentStory,
isScanInProgress = homeState.value.scanInProgress, isScanInProgress = state.scanInProgress,
onGoToPreviousStory = goToPreviousStory, onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory, onGoToNextStory = goToNextStory,
onSearchTokensClick = onSearchTokensClick, onSearchTokensClick = onSearchTokensClick,

View file

@ -1,6 +1,6 @@
@file:Suppress("MagicNumber") @file:Suppress("MagicNumber")
package com.tangem.tap.features.home.compose package com.tangem.features.home.impl.ui.compose
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -20,23 +20,22 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.* import com.tangem.features.home.impl.ui.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtonsV2 import com.tangem.features.home.impl.ui.compose.views.HomeButtonsV2
import com.tangem.tap.features.home.compose.views.StoriesProgressBar import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState import com.tangem.features.home.impl.ui.state.Stories
import com.tangem.tap.features.home.redux.Stories
import com.tangem.wallet.R
import kotlin.math.max import kotlin.math.max
import com.tangem.core.ui.R
import com.tangem.features.home.impl.ui.state.HomeUM
@Composable @Composable
internal fun StoriesScreenV2( internal fun StoriesScreenV2(
homeState: MutableState<HomeState>, state: HomeUM,
onCreateNewWalletButtonClick: () -> Unit, onCreateNewWalletButtonClick: () -> Unit,
onAddExistingWalletButtonClick: () -> Unit, onAddExistingWalletButtonClick: () -> Unit,
onScanButtonClick: () -> Unit, onScanButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
val state = homeState.value
var currentStory by remember { mutableStateOf(state.firstStory) } var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
@ -55,14 +54,14 @@ internal fun StoriesScreenV2(
// todo refactor [REDACTED_TASK_KEY] // todo refactor [REDACTED_TASK_KEY]
StoriesScreenContentV2( StoriesScreenContentV2(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER), .testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentV2Config( config = StoriesScreenContentV2Config(
storiesSize = state.stories.lastIndex, storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex, currentStoryIndex = currentStoryIndex,
currentStory = currentStory, currentStory = currentStory,
isScanInProgress = homeState.value.scanInProgress, isScanInProgress = state.scanInProgress,
onGoToPreviousStory = goToPreviousStory, onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory, onGoToNextStory = goToNextStory,
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick, onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.content package com.tangem.features.home.impl.ui.compose.content
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -21,9 +21,9 @@ import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation import com.tangem.features.home.impl.ui.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation
import com.tangem.wallet.R import com.tangem.core.ui.R
@Composable @Composable
fun StoriesRevolutionaryWallet() { fun StoriesRevolutionaryWallet() {

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.content package com.tangem.features.home.impl.ui.compose.content
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -17,12 +17,10 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.extensions.dpSize import com.tangem.features.home.impl.ui.compose.HorizontalSlidingImage
import com.tangem.tap.common.compose.extensions.halfHeight import com.tangem.core.ui.R
import com.tangem.tap.common.compose.extensions.toPx import com.tangem.core.ui.utils.dpSize
import com.tangem.tap.common.extensions.isEven import com.tangem.core.ui.utils.toPx
import com.tangem.tap.features.home.compose.HorizontalSlidingImage
import com.tangem.wallet.R
@Composable @Composable
fun StoriesCurrenciesContent(paused: Boolean, duration: Int) { fun StoriesCurrenciesContent(paused: Boolean, duration: Int) {
@ -138,4 +136,8 @@ private val BottomGradient: Brush = Brush.verticalGradient(
TangemColorPalette.Black.copy(alpha = 0.95f), TangemColorPalette.Black.copy(alpha = 0.95f),
TangemColorPalette.Black, TangemColorPalette.Black,
), ),
) )
fun DpSize.halfHeight(): Dp = this.height / 2
fun Int.isEven() = this and 1 == 0

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.content package com.tangem.features.home.impl.ui.compose.content
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
@ -25,8 +25,8 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesTextAnimation import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation
import com.tangem.wallet.R import com.tangem.core.ui.R
@Suppress("LongMethod", "ComplexMethod", "MagicNumber") @Suppress("LongMethod", "ComplexMethod", "MagicNumber")
@Composable @Composable

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.content package com.tangem.features.home.impl.ui.compose.content
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import com.tangem.tap.common.compose.extensions.AnimatedValue import com.tangem.core.ui.R
import com.tangem.tap.common.compose.extensions.asImageBitmap import com.tangem.core.ui.utils.AnimatedValue
import com.tangem.tap.common.compose.extensions.toAnimatable import com.tangem.core.ui.utils.asImageBitmap
import com.tangem.wallet.R import com.tangem.core.ui.utils.toAnimatable
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.views package com.tangem.features.home.impl.ui.compose.views
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R import com.tangem.core.ui.R
@Composable @Composable
internal fun HomeButtons( internal fun HomeButtons(

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.views package com.tangem.features.home.impl.ui.compose.views
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R import com.tangem.core.ui.R
@Composable @Composable
internal fun HomeButtonsV2( internal fun HomeButtonsV2(

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.views package com.tangem.features.home.impl.ui.compose.views
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -12,7 +12,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R import com.tangem.core.ui.R
@Composable @Composable
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) { internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.views package com.tangem.features.home.impl.ui.compose.views
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose.views package com.tangem.features.home.impl.ui.compose.views
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable

View file

@ -1,15 +1,16 @@
package com.tangem.tap.features.home.redux package com.tangem.features.home.impl.ui.state
import kotlinx.collections.immutable.ImmutableList 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 {
data class HomeUM(
val scanInProgress: Boolean,
val stories: ImmutableList<Stories>,
val onScanClick: () -> Unit,
val onShopClick: () -> Unit,
val onSearchTokensClick: () -> Unit,
val onCreateNewWalletClick: () -> Unit,
val onAddExistingWalletClick: () -> Unit,
) {
val firstStory: Stories get() = stories[0] val firstStory: Stories get() = stories[0]
fun stepOf(story: Stories): Int = stories.indexOf(story) fun stepOf(story: Stories): Int = stories.indexOf(story)

View file

@ -206,7 +206,7 @@ internal class OnboardingEntryModel @Inject constructor(
router.replaceAll(AppRoute.Wallet) router.replaceAll(AppRoute.Wallet)
} }
} else { } else {
router.replaceAll(AppRoute.Home) router.replaceAll(AppRoute.Home())
} }
} }

View file

@ -197,7 +197,7 @@ internal class WalletSettingsModel @Inject constructor(
if (hasUserWallets) { if (hasUserWallets) {
router.pop() router.pop()
} else { } else {
router.replaceAll(AppRoute.Home) router.replaceAll(AppRoute.Home())
} }
} }

View file

@ -102,7 +102,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
} else { } else {
tokenListStore.clear() tokenListStore.clear()
stateHolder.clear() stateHolder.clear()
appRouter.replaceAll(AppRoute.Home) appRouter.replaceAll(AppRoute.Home())
} }
} }
} }

View file

@ -75,7 +75,7 @@ internal class DefaultWalletRouter @Inject constructor(
} }
override fun openStoriesScreen() { override fun openStoriesScreen() {
router.push(AppRoute.Home) router.push(AppRoute.Home())
} }
override fun isWalletLastScreen(): Boolean { override fun isWalletLastScreen(): Boolean {

View file

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

View file

@ -183,6 +183,9 @@ include(":libs:tangem-sdk-api")
include(":features:onboarding-v2:api") include(":features:onboarding-v2:api")
include(":features:onboarding-v2:impl") include(":features:onboarding-v2:impl")
include(":features:home:api")
include(":features:home:impl")
include(":features:referral:api") include(":features:referral:api")
include(":features:referral:data") include(":features:referral:data")
include(":features:referral:domain") include(":features:referral:domain")