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

@ -176,13 +176,22 @@ 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
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>()
@ -344,12 +353,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)
}
}
@ -435,9 +442,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
@ -452,7 +465,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)
shouldShowInitialPush -> AppRoute.PushNotification
else -> AppRoute.Home
else -> AppRoute.Home(launchMode = launchMode)
}
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.ScanResponse
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.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

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

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

@ -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()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit)
}

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

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

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

@ -20,10 +20,8 @@ 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)
}
}

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