Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-05 11:53:07 +03:00
commit 8fe3e9e3c1
974 changed files with 23162 additions and 21412 deletions

View file

@ -109,6 +109,8 @@ dependencies {
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.common)
implementation(projects.common.routing)
@ -152,9 +154,10 @@ dependencies {
implementation(projects.data.nft)
implementation(projects.data.onramp)
implementation(projects.data.networks)
implementation(projects.data.quotes)
implementation(projects.data.blockaid)
/** Features */
implementation(projects.features.onboarding)
implementation(projects.features.referral.impl)
implementation(projects.features.referral.domain)
implementation(projects.features.referral.data)
@ -273,6 +276,7 @@ dependencies {
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.coil)
implementation(deps.coil.gif)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
@ -318,6 +322,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -43,6 +43,7 @@
android:resizeableActivity="@bool/resizeable_activity"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
@ -127,7 +128,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="onramp-success"
android:host="*"
android:scheme="tangem" />
</intent-filter>

@ -1 +1 @@
Subproject commit 29f73d909e14cb3760118b197b1929a6088bc40e
Subproject commit a20134ee4be4d7e1e34c6f44089b3cb0713eacc3

View file

@ -1,65 +0,0 @@
package com.tangem.tap
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.utils.Provider
import dagger.hilt.android.AndroidEntryPoint
import java.util.WeakHashMap
import javax.inject.Inject
@AndroidEntryPoint
internal class DecomposeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val component by lazy(LazyThreadSafetyMode.NONE) {
val tag = requireArguments().getString(TAG_KEY)
val builder = componentsBuilders[tag]
requireNotNull(builder?.build()) {
"Component builder is not set, call newInstance() for DecomposeFragment creation first."
}
}
@Composable
override fun ScreenContent(modifier: Modifier) {
component.Content(modifier)
}
private class ComponentBuilder<C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>>(
private val contextProvider: Provider<AppComponentContext>,
private val params: P,
private val componentFactory: F,
) {
fun build(): C = componentFactory.create(contextProvider(), params)
}
companion object {
private const val TAG_KEY = "tag"
private val componentsBuilders = WeakHashMap<String, ComponentBuilder<*, *, *>>()
fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>> newInstance(
tag: String,
contextProvider: Provider<AppComponentContext>,
params: P,
componentFactory: F,
): Fragment {
this@Companion.componentsBuilders[tag] = ComponentBuilder(contextProvider, params, componentFactory)
return DecomposeFragment().apply {
this.arguments = bundleOf(TAG_KEY to tag)
}
}
}
}

View file

@ -12,7 +12,6 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
@ -20,19 +19,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.arkivanov.decompose.value.subscribe
import com.arkivanov.essenty.lifecycle.asEssentyLifecycle
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -41,12 +32,6 @@ import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
@ -62,37 +47,28 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.toggle.RoutingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -105,7 +81,6 @@ lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
@ -117,7 +92,7 @@ val mainScope = CoroutineScope(mainCoroutineContext)
@Suppress("LargeClass")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var appStateHolder: AppStateHolder
@ -181,9 +156,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var backupServiceHolder: BackupServiceHolder
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
@Inject
lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase
@ -193,9 +165,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var dispatchers: CoroutineDispatcherProvider
@Inject
internal lateinit var routingFeatureToggles: RoutingFeatureToggles
@Inject
internal lateinit var uiDependencies: UiDependencies
@ -209,9 +178,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -248,13 +215,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
installActivityDependencies()
observeAppThemeModeUpdates()
if (routingFeatureToggles.isNavigationRefactoringEnabled) {
setRootContent()
} else {
setContentView(R.layout.activity_main)
installRouting()
installEventMessageEffect()
}
setRootContent()
initContent()
@ -271,33 +232,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lifecycle.addObserver(defaultDeviceFlipDetector)
}
private fun installEventMessageEffect() {
binding.composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
binding.composeView.setContent {
TangemTheme(
activity = this,
uiDependencies = uiDependencies,
overrideSystemBarColors = false,
) {
EventMessageEffect(
onShowSnackbar = { message, _ ->
showSnackbar(
text = message.message.resolveReference(resources),
length = when (message.duration) {
SnackbarMessage.Duration.Short -> Snackbar.LENGTH_SHORT
SnackbarMessage.Duration.Long -> Snackbar.LENGTH_LONG
SnackbarMessage.Duration.Indefinite -> Snackbar.LENGTH_INDEFINITE
},
buttonTitle = message.actionLabel?.resolveReference(resources),
action = message.action,
onDismiss = message.onDismissRequest,
)
},
)
}
}
}
private fun setRootContent() {
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
@ -312,64 +246,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
private fun installRouting() {
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
// we try to init component with previous stack
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
initialStack = appRouterConfig.stack,
)
appRouterConfig.routerScope = mainScope
appRouterConfig.componentRouter = routingComponent.router
appRouterConfig.snackbarHandler = this
routingComponent.stack.subscribe(lifecycle.asEssentyLifecycle()) { childStack ->
val stack = childStack.backStack
.plus(childStack.active)
.map { it.configuration }
if (stack == appRouterConfig.stack) return@subscribe
appRouterConfig.stack = stack
if (stack.size == 1) {
supportFragmentManager.run {
val activeChildName = stack.first().path
(0 until backStackEntryCount)
.mapNotNull { getBackStackEntryAt(it).name }
.filter { it != activeChildName }
.forEach {
popBackStackImmediate(it, POP_BACK_STACK_INCLUSIVE)
}
}
}
when (val child = childStack.active.instance) {
is RoutingComponent.Child.Initial -> Unit
is RoutingComponent.Child.LegacyFragment -> {
supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider)
}
is RoutingComponent.Child.LegacyIntent -> {
startActivity(child.intent)
}
is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child")
}
}
}
private fun installActivityDependencies() {
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
} else {
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
}
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
lockUserWalletsTimer = LockUserWalletsTimer(
context = this,
@ -439,17 +321,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onResume() {
super.onResume()
if (!routingFeatureToggles.isNavigationRefactoringEnabled) {
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
}
navigateToInitialScreenIfNeeded(intent)
}
override fun onStop() {
notificationsHandler = null
dialogManager.onStop()
super.onStop()
}
@ -482,31 +357,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually change the background color of the activity when the UI mode changes to prevent
* flickering when navigating between fragments.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
if (routingFeatureToggles.isNavigationRefactoringEnabled.not()) {
updateAppBackground()
}
}
private fun updateAppBackground() {
val backgroundColor = if (isDarkTheme()) {
TangemColorPalette.Dark6
} else {
TangemColorPalette.White
}
findViewById<CoordinatorLayout>(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb())
}
private fun isDarkTheme(): Boolean {
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> true
@ -528,29 +378,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
override fun showSnackbar(@StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: (() -> Unit)?) {
showSnackbar(
getString(text),
length,
buttonTitle?.let(::getString),
action = { action?.invoke() },
)
}
override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) {
showSnackbar(
text = text.resolveReference(resources),
length = length,
buttonTitle = buttonTitle?.resolveReference(resources),
action = { action?.invoke() },
)
}
override fun dismissSnackbar() {
snackbar?.dismiss()
snackbar = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -578,40 +405,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
return if (result) super.dispatchTouchEvent(event) else false
}
private fun showSnackbar(
text: String,
length: Int,
buttonTitle: String?,
action: (() -> Unit)? = null,
onDismiss: () -> Unit = {},
) {
if (snackbar != null) return
snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply {
val textColor = getColor(R.color.text_primary_2)
setBackgroundTint(getColor(R.color.button_primary))
setActionTextColor(textColor)
setTextColor(textColor)
if (buttonTitle != null && action != null) {
setAction(buttonTitle, { action() })
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
onDismiss()
snackbar = null
removeCallback(this)
}
},
)
}
snackbar?.show()
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
@ -662,7 +455,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
store.dispatch(BackupAction.CheckForUnfinishedBackup)
viewModel.checkForUnfinishedBackup()
}
private fun observePolkadotAccountHealthCheck() {

View file

@ -1,6 +1,9 @@
package com.tangem.tap
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil.ImageLoader
@ -71,9 +74,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import org.rekotlin.Store
import kotlin.collections.set
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -228,12 +229,32 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
// endregion
private val appScope = MainScope()
override fun onCreate() {
enableStrictModeInDebug()
super.onCreate()
init()
}
updateLogFiles()
private fun enableStrictModeInDebug() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build(),
)
}
}
private fun updateLogFiles() {
@ -260,18 +281,28 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
async {
featureTogglesManager.init()
},
async {
excludedBlockchainsManager.init()
},
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
appScope.launch {
launch(Dispatchers.IO) {
loadNativeLibraries()
updateLogFiles()
}
}
loadNativeLibraries()
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
@ -287,9 +318,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
appStateHolder.mainStore = store
walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId)
appStateHolder.mainStore = store
walletConnect2Repository.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
}
private fun createReduxStore(): Store<AppState> {

View file

@ -9,9 +9,11 @@ import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -56,10 +58,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
messageRes = R.string.nfc_error_unavailable,
context = context,
)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
@ -143,9 +141,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,
@ -154,10 +149,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
unfinishedBackupScanResponse = state.dialog.scanResponse,
)
is BackupDialog.ResetBackupCard -> ResetBackupCardDialog.create(
context = context,
cardId = state.dialog.cardId,
)
is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(

View file

@ -1,16 +1,5 @@
package com.tangem.tap.common
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
@ -21,41 +10,4 @@ object TestActions {
var testAmountInjectionForWalletManagerEnabled = false
}
typealias TestAction = Pair<String, () -> Unit>
class TestActionsBottomSheetDialog(
private val appDialog: AppDialog.TestActionsDialog,
context: Context,
) : BottomSheetDialog(context) {
@Suppress("MagicNumber")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createContentView())
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = 400
setOnCancelListener {
store.dispatchDialogHide()
}
}
private fun createContentView(): View {
val actionButtons = mutableListOf<View>()
appDialog.actionsList.forEach { (actionName, action) ->
val view = Button(context).apply {
text = actionName
setOnClickListener {
action()
cancel()
}
}
actionButtons.add(view)
}
return LinearLayoutCompat(context).apply {
orientation = LinearLayoutCompat.VERTICAL
actionButtons.forEach { addView(it) }
}
}
}
typealias TestAction = Pair<String, () -> Unit>

View file

@ -1,60 +0,0 @@
package com.tangem.tap.common.extensions
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.tangem.utils.Provider
import com.tangem.wallet.R
import timber.log.Timber
fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider<Fragment>) {
if (backStackEntryCount > 0) {
val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name
if (name == currentFragmentName) {
Timber.i("Fragment $name is already at the top of the stack")
return
}
}
Timber.i("Showing $name route")
val isPoppedBack = popBackStackImmediate(name, 0)
if (!isPoppedBack) {
val fragment = fragmentProvider()
if (fragment is DialogFragment) {
fragment.showDialog(fragmentManager = this, name)
} else {
fragment.showFragment(fragmentManager = this, name)
}
Timber.i("Route $name is shown")
} else {
Timber.i("Route $name is found in backstack and shown")
}
}
private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
try {
transaction.addToBackStack(name)
show(transaction, name)
} catch (e: IllegalStateException) {
transaction.add(this, name)
transaction.addToBackStack(name)
transaction.commitAllowingStateLoss()
}
}
private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, this, name)
transaction.addToBackStack(name)
transaction.commitAllowingStateLoss()
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.extensions
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.UserWallet
@ -43,20 +45,16 @@ suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
state.globalState.tapWalletManager.onWalletSelected(userWallet)
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.ShowErrorNotification(error))
}
/**
* @param fatal used to indicate errors that should not normally occur
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
fun Store<AppState>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.DebugShowErrorNotification(error))
fun Store<AppState>.dispatchDebugErrorNotification(error: TapError) {
inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error")))
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.images
import android.content.Context
import android.os.Build
import android.util.Log
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.util.Logger
@ -25,6 +28,15 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.build()
}
}
.components {
// According to Coil ImageDecoder API is faster and supports animated WebP and HEIF
// https://coil-kt.github.io/coil/gifs/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder(context)

View file

@ -2,9 +2,6 @@ package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.TestAction
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.wallet.R
/**
@ -18,15 +15,6 @@ sealed class AppDialog : StateDialog {
val onOk: VoidCallback? = null,
) : AppDialog()
internal data class AddressInfoDialog(
val currency: Currency,
val addressData: WalletAddressData,
) : AppDialog()
data class TestActionsDialog(
val actionsList: List<TestAction>,
) : AppDialog()
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,

View file

@ -4,11 +4,6 @@ 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.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -20,14 +15,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}

View file

@ -9,17 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddlewa
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.note.redux.OnboardingNoteMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState
@ -31,14 +21,9 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -46,18 +31,12 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware,
notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,

View file

@ -1,141 +0,0 @@
package com.tangem.tap.common.redux
import android.content.Context
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.domain.ArgError
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
private val basicCoordinatorLayout = WeakReference(coordinatorLayout)
private var baseLayout = basicCoordinatorLayout
private fun showNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar -> snackbar.show() }
}
}
private fun showDebugNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar ->
snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning))
snackbar.show()
}
}
}
fun showNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
showNotification(getMessageString(it.context, message, args))
}
}
fun showToastNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
Toast.makeText(it.context, getMessageString(it.context, message, args), Toast.LENGTH_LONG).show()
}
}
fun showNotification(errorList: List<Pair<Int, List<Any>?>>, builder: (List<String>) -> String) {
val context = baseLayout.get()?.context ?: return
val message = builder(errorList.map { getMessageString(context, it.first, it.second) })
showNotification(message)
}
fun showDebugErrorNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
showDebugNotification(getMessageString(it.context, message, args))
}
}
}
fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
return if (args.isNullOrEmpty()) {
context.getString(message)
} else {
context.getString(message, *args.toTypedArray())
}
}
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNotificationAction(action)
next(action)
}
}
}
@Suppress("ComplexMethod")
private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> {
notificationsHandler?.showNotification(action.messageResource)
}
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action) {
is Debug -> {
val args = (action.error as? ArgError)?.args ?: listOf()
when (action) {
is DebugNotification -> {
notificationsHandler?.showNotification(action.error.messageResource, args)
}
is DebugToastNotification -> {
notificationsHandler?.showToastNotification(action.error.messageResource, args)
}
is DebugErrorAction -> {
notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args)
}
}
}
else -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
}
}
}
}
}
}
}
interface ToastNotificationAction : Action {
val messageResource: Int
}
interface NotificationAction : Action {
val messageResource: Int
}
interface ErrorAction : Action {
val error: TapError
}
// Processed only in the debug builds
interface Debug
interface DebugNotification : Debug, NotificationAction
interface DebugToastNotification : Debug, ToastNotificationAction
interface DebugErrorAction : Debug, ErrorAction

View file

@ -5,45 +5,14 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import org.rekotlin.Action
sealed class GlobalAction : Action {
// notifications
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding : GlobalAction() {
/**
* Initiate an onboarding process.
* For resuming unfinished backup of standard Wallet see
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data class Start(
val scanResponse: ScanResponse,
val source: BackupStartedSource,
val canSkipBackup: Boolean = true,
) : Onboarding()
/**
* Initiate resuming of unfinished backup for standard Wallet.
* See more BackupAction.CheckForUnfinishedBackup
*/
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
object Stop : Onboarding()
data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding()
}
object ScanFailsCounter {
data class ChooseBehavior(
val result: CompletionResult<ScanResponse>,

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -12,21 +11,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Start -> {
val onboardingManager = OnboardingManager(action.scanResponse)
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
globalState.copy(onboardingState = OnboardingState(true, null))
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingState = OnboardingState(false))
}
is GlobalAction.Onboarding.ShouldResetCardOnCreate -> {
globalState.copy(
onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset),
)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}

View file

@ -5,10 +5,8 @@ import com.tangem.domain.redux.LegacyAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -26,15 +24,6 @@ internal object LegacyMiddleware {
{ next ->
{ action ->
when (action) {
is LegacyAction.StartOnboardingProcess -> {
store.dispatch(
GlobalAction.Onboarding.Start(
scanResponse = action.scanResponse,
source = BackupStartedSource.CreateBackup,
canSkipBackup = action.canSkipBackup,
),
)
}
is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)

View file

@ -1,118 +0,0 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
import android.view.ViewGroup
import android.view.animation.*
import android.widget.ViewSwitcher
import com.tangem.tap.common.entities.ProgressState
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
class RefreshBalanceWidget(root: ViewGroup) : ViewStateWidget {
var isShowing: Boolean? = null
private set
override val mainView: View = root.findViewById(R.id.btn_refresh_balance)
private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) }
private val arrowView = mainView.findViewById<View>(R.id.imv_arrow_refresh)
private var progressViewAnimation: Animation? = null
init {
viewSwitcher.inAnimation = AlphaAnimation(0f, 1f).apply {
duration = 400
interpolator = LinearInterpolator()
}
viewSwitcher.outAnimation = AlphaAnimation(1f, 0f).apply {
duration = 400
interpolator = LinearInterpolator()
}
}
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
val currentStateByView = getState()
when {
currentStateByView == progressState -> return
currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return
}
mainView.isClickable = currentStateByView == ProgressState.Done || currentStateByView == ProgressState.Error
animateState(progressState)
viewSwitcher.showNext()
}
@Suppress("MagicNumber")
private fun animateState(state: ProgressState) {
when (state) {
ProgressState.Done, ProgressState.Error -> {
progressViewAnimation?.cancel()
progressViewAnimation = null
}
ProgressState.Loading -> {
progressViewAnimation = RotateAnimation(
/* fromDegrees = */
0f,
/* toDegrees = */
360f,
/* pivotXType = */
Animation.RELATIVE_TO_SELF,
/* pivotXValue = */
0.5f,
/* pivotYType = */
Animation.RELATIVE_TO_SELF,
/* pivotYValue = */
0.5f,
)
progressViewAnimation?.duration = 700
progressViewAnimation?.interpolator = AccelerateInterpolator()
progressViewAnimation?.repeatCount = -1
progressViewAnimation?.let { arrowView.startAnimation(it) }
}
else -> {}
}
}
fun show(show: Boolean) {
if (show == isShowing) return
isShowing = show
if (show) {
mainView.startAnimation(ShowAnimation())
} else {
mainView.startAnimation(AlphaAnimation(1f, 0f).apply { fillAfter = true })
}
}
private fun isArrowRefreshActive(): Boolean = viewSwitcher.currentView.id == R.id.fl_arrow_refresh_container
private fun getState(): ProgressState = if (isArrowRefreshActive()) ProgressState.Done else ProgressState.Loading
}
@Suppress("MagicNumber")
class ShowAnimation : AnimationSet(true) {
init {
addAnimation(
ScaleAnimation(
0f,
1f,
0f,
1f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f,
),
)
addAnimation(AlphaAnimation(0f, 1f))
duration = 300
interpolator = AnticipateOvershootInterpolator()
}
}

View file

@ -1,75 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
/**
[REDACTED_AUTHOR]
*/
internal class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context,
) : BottomSheetDialog(context) {
var binding: DialogOnboardingAddressInfoBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogOnboardingAddressInfoBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
setOnCancelListener {
store.dispatchDialogHide()
binding = null
}
}
override fun show() {
super.show()
Analytics.send(Token.Receive.ScreenOpened(stateDialog.currency.currencySymbol))
showData(data = stateDialog.addressData)
}
private fun showData(data: WalletAddressData) = with(binding!!) {
pseudoToolbar.imvClose.setOnClickListener {
dismissWithAnimation = true
cancel()
}
imvQrCode.setImageBitmap(data.shareUrl.toQrCode())
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Receive.ButtonCopyAddress())
Toast
.makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT)
.show()
store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Receive.ButtonShareAddress())
store.dispatchShare(data.shareUrl)
}
val blockchain = stateDialog.currency.blockchain
tvReceiveMessage.text = tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
blockchain.getCoinName(),
blockchain.currency,
blockchain.fullName,
)
}
}

View file

@ -20,18 +20,22 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaAuthTokenStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
)
}
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)

View file

@ -20,12 +20,14 @@ class DefaultVisaOTPStorage @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaOTPStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
}
override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) {
secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId)

View file

@ -0,0 +1,11 @@
package com.tangem.tap.data
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await
internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
return FirebaseMessaging.getInstance().token.await()
}
}

View file

@ -24,7 +24,7 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
}
override suspend fun getSyncStrict(key: UserWalletId): UserWallet {
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.data
import com.tangem.tap.data.FirebasePushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
@Binds
@Singleton
fun bindPushNotificationsTokenProvider(impl: FirebasePushNotificationsTokenProvider): PushNotificationsTokenProvider
}

View file

@ -5,6 +5,8 @@ import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -73,6 +75,8 @@ internal object ManageTokensDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
@ -84,6 +88,8 @@ internal object ManageTokensDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -5,6 +5,9 @@ import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -49,8 +52,16 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository)
fun provideGetTokenQuotesUseCase(
quotesRepository: QuotesRepository,
singleQuoteSupplier: SingleQuoteSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(
quotesRepository = quotesRepository,
singleQuoteSupplier = singleQuoteSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@ -63,6 +74,8 @@ object MarketsDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
@ -73,6 +86,8 @@ object MarketsDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.crypto.bip39.Wordlist
import com.tangem.feature.onboarding.data.DefaultMnemonicRepository
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.sdk.extensions.getWordlist
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class MnemonicModule {
@Provides
@Singleton
fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository {
return DefaultMnemonicRepository(Wordlist.getWordlist(context))
}
}

View file

@ -35,6 +35,16 @@ internal object NFTDomainModule {
nftRepository = nftRepository,
)
@Provides
@Singleton
fun providesRefreshAllNFTUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
): RefreshAllNFTUseCase = RefreshAllNFTUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
@Provides
@Singleton
fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase =
@ -47,7 +57,7 @@ internal object NFTDomainModule {
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
): GetNFTAvailableNetworksUseCase = GetNFTAvailableNetworksUseCase(
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
)
@ -66,4 +76,11 @@ internal object NFTDomainModule {
GetNFTNetworkStatusUseCase(
networksRepository = networksRepository,
)
@Provides
@Singleton
fun providesGetNFTExploreUrlUseCase(nftRepository: NFTRepository): GetNFTExploreUrlUseCase =
GetNFTExploreUrlUseCase(
nftRepository = nftRepository,
)
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.di.domain
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.utils.notifications.PushNotificationsTokenProvider
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 NotificationsDomainModule {
@Provides
@Singleton
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase =
GetApplicationIdUseCase(
notificationsRepository = notificationsRepository,
)
@Provides
@Singleton
fun providesSendPushTokenUseCase(
notificationsRepository: NotificationsRepository,
getApplicationIdUseCase: GetApplicationIdUseCase,
pushNotificationsTokenProvider: PushNotificationsTokenProvider,
): SendPushTokenUseCase = SendPushTokenUseCase(
notificationsRepository = notificationsRepository,
getApplicationIdUseCase = getApplicationIdUseCase,
pushNotificationsTokenProvider = pushNotificationsTokenProvider,
)
}

View file

@ -1,7 +1,11 @@
package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
import dagger.Provides
@ -92,10 +96,12 @@ internal object StakingDomainModule {
fun provideFetchStakingYieldBalanceUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
)
}

View file

@ -7,7 +7,13 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
@ -35,6 +41,8 @@ internal object TokensDomainModule {
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
@ -43,6 +51,8 @@ internal object TokensDomainModule {
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -55,6 +65,8 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
@ -63,6 +75,8 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -168,6 +182,8 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
@ -176,6 +192,8 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -188,6 +206,8 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
@ -196,6 +216,8 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -364,10 +386,14 @@ internal object TokensDomainModule {
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
multiQuoteFetcher: MultiQuoteFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
multiQuoteFetcher = multiQuoteFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -386,20 +412,30 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -410,21 +446,37 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository)
}
}

View file

@ -30,6 +30,21 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideGetEthSpecificFeeUseCase(walletManagersFacade: WalletManagersFacade): GetEthSpecificFeeUseCase {
return GetEthSpecificFeeUseCase(walletManagersFacade = walletManagersFacade)
}
@Provides
@Singleton
fun provideTransferGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetTransferFeeUseCase {
return GetTransferFeeUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(),
)
}
@Provides
@Singleton
fun provideSendTransactionUseCase(
@ -147,4 +162,38 @@ internal object TransactionDomainModule {
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
return GetAllowanceUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideCreateTransferTransactionUseCase(
transactionRepository: TransactionRepository,
): CreateTransferTransactionUseCase {
return CreateTransferTransactionUseCase(transactionRepository)
}
@Provides
@Singleton
fun providePrepareForSendUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
): PrepareForSendUseCase {
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideSignUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
): SignUseCase {
return SignUseCase(cardSdkConfigRepository, walletManagersFacade)
}
@Provides
@Singleton
fun provideCreateNFTTransferTransactionUseCase(
transactionRepository: TransactionRepository,
): CreateNFTTransferTransactionUseCase {
return CreateNFTTransferTransactionUseCase(transactionRepository)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,4 +21,10 @@ internal object WalletConnectDomainModule {
): CheckIsWalletConnectAvailableUseCase {
return CheckIsWalletConnectAvailableUseCase(walletConnectRepository = walletConnectRepository)
}
@Provides
@Singleton
fun providesWcSessionsUseCase(sessionsManager: WcSessionsManager): WcSessionsUseCase {
return WcSessionsUseCase(sessionsManager)
}
}

View file

@ -14,6 +14,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -172,8 +174,16 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase {
return GetCardImageUseCase(verifier = onlineCardVerifier)
fun provideGetCardImageUseCase(
onlineCardVerifier: OnlineCardVerifier,
cardArtworksProvider: CardArtworksProvider,
cardSdkFeatureToggles: CardSdkFeatureToggles,
): GetCardImageUseCase {
return GetCardImageUseCase(
verifier = onlineCardVerifier,
cardArtworksProvider = cardArtworksProvider,
cardSdkFeatureToggles = cardSdkFeatureToggles,
)
}
@Provides

View file

@ -7,7 +7,6 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -42,7 +41,6 @@ class TapWalletManager(
withMainContext {
// Order is important
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
@ -37,7 +38,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(
if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}

View file

@ -21,7 +21,6 @@ import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -34,9 +33,6 @@ import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@ -208,8 +204,6 @@ internal class LegacyScanProcessor @Inject constructor(
crossinline onCancel: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
checkCardWasUsedInApp(
scanResponse = scanResponse,
onCancel = {
@ -222,38 +216,26 @@ internal class LegacyScanProcessor @Inject constructor(
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
source = BackupStartedSource.Onboarding,
canSkipBackup = scanResponse.card.canSkipBackup,
mode = AppRoute.Onboarding.Mode.Onboarding,
),
)
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
if (isTwinRefactoringEnabled) {
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
}
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)

View file

@ -5,21 +5,15 @@ import arrow.core.right
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
@ -40,46 +34,30 @@ class CheckForOnboardingChain(
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
source = BackupStartedSource.Onboarding,
canSkipBackup = previousChainResult.card.canSkipBackup,
mode = AppRoute.Onboarding.Mode.Onboarding,
),
)
val route = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(route).left()
).left()
}
else -> {
Analytics.setContext(previousChainResult)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) {
if (isTwinRefactoringEnabled) {
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
).left()
} else {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
}
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()

View file

@ -4,14 +4,12 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.*
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
@ -46,7 +44,7 @@ import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.measureTimedValue
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val mode: VisaCardActivationTaskMode,
@Assisted private val activationInput: VisaActivationInput,
@ -97,7 +95,23 @@ class VisaCardActivationTask @AssistedInject constructor(
context.signAuthorizationChallenge(mode.authorizationChallenge)
}
is VisaCardActivationTaskMode.SignOnly -> {
context.signData(mode.dataToSignByCardWallet)
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
context.signData(
mode.dataToSignByCardWallet,
derivedPublicKey,
)
}
}
}
@ -138,9 +152,35 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.processSignedAuthorizationChallenge(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
when (val createWalletResult = createWallet()) {
is CompletionResult.Failure -> return CompletionResult.Failure(createWalletResult.error)
is CompletionResult.Success -> {}
}
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
return coroutineScope {
val dataToSignDeferred = async { getDataToSign(signedChallenge) }
val otpTaskDeferred = async { createWallet() }
val dataToSignDeferred =
async { getDataToSign(signedChallenge = signedChallenge, cardWalletAddress = walletAddress) }
val otpTaskDeferred = async { createOTP() }
val dataToSign = dataToSignDeferred.await()
.getOrElse {
@ -150,12 +190,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signData(dataToSign)
signData(
dataToSign = dataToSign,
derivedPublicKey = derivedPublicKey,
)
}
}
private suspend fun SessionContext.getDataToSign(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
catch(
block = {
@ -168,7 +212,12 @@ class VisaCardActivationTask @AssistedInject constructor(
raise(VisaActivationError.WrongRemoteState.tangemError)
}
visaActivationRepository.getCardWalletAcceptanceData(remoteState.request)
visaActivationRepository.getCardWalletAcceptanceData(
VisaCardWalletDataToSignRequest(
activationOrderInfo = remoteState.activationOrderInfo,
cardWalletAddress = cardWalletAddress,
),
)
},
catch = {
raise(VisaAuthorizationAPIError.tangemError)
@ -182,7 +231,7 @@ class VisaCardActivationTask @AssistedInject constructor(
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return if (card.wallets.any { it.curve == EllipticCurve.Secp256k1 }) {
createOTP()
CompletionResult.Success(Unit)
} else {
val createWalletTask = CreateWalletTask(EllipticCurve.Secp256k1)
@ -199,7 +248,7 @@ class VisaCardActivationTask @AssistedInject constructor(
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
createOTP()
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
@ -245,26 +294,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.signData(
dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
): CompletionResult<VisaCardActivationResponse> {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
val task = SignHashCommand(
hash = dataToSign.hashToSign.hexToBytes(),
walletPublicKey = wallet.publicKey,
@ -286,8 +324,8 @@ class VisaCardActivationTask @AssistedInject constructor(
Timber.i("SignHashCommand success")
handleSignedData(
dataToSign = dataToSign,
walletAddress = walletAddress,
response = result.data,
derivedPublicKey = derivedPublicKey,
)
}
is CompletionResult.Failure -> {
@ -313,7 +351,7 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.handleSignedData(
dataToSign: VisaDataToSignByCardWallet,
walletAddress: String,
derivedPublicKey: ExtendedPublicKey,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val otp = otpStorage.getOTP(cardId) ?: run {
@ -321,11 +359,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError)
}
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = response.signature,
hash = dataToSign.hashToSign.hexToBytes(),
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString()
val signedActivationData = dataToSign.sign(
cardWalletAddress = walletAddress,
rootOTP = otp.rootOTP.toHexString(),
otpCounter = otp.counter,
signature = response.signature.toHexString(),
signature = rsvSignature,
)
return setupAccessCode().map {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
@ -10,6 +11,7 @@ import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
@ -129,6 +131,7 @@ class VisaCustomerWalletApproveTask(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
extendedPublicKey = extendedPublicKey,
callback = callback,
)
}
@ -149,6 +152,7 @@ class VisaCustomerWalletApproveTask(
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
extendedPublicKey = null,
session = session,
callback = callback,
)
@ -157,11 +161,14 @@ class VisaCustomerWalletApproveTask(
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val signTask = SignHashCommand(
hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(),
hash = hashToSign,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -169,11 +176,18 @@ class VisaCustomerWalletApproveTask(
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature,
hash = hashToSign,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString()
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = result.data.signature.toHexString(),
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
)

View file

@ -9,4 +9,10 @@ internal class DefaultTokensFeatureToggles(
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
override val isQuotesLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED")
override val isStakingLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED")
}

View file

@ -21,8 +21,6 @@ internal data class UserWalletPublicInformation(
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "scanResponse")

View file

@ -20,9 +20,12 @@ internal class DefaultUserWalletsPublicInformationRepository(
moshi: Moshi,
private val secureStorage: SecureStorage,
) : UserWalletsPublicInformationRepository {
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> by lazy {
moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {

View file

@ -24,12 +24,15 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
private val secureStorage: SecureStorage,
) : UserWalletsSensitiveInformationRepository {
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
UserWalletSensitiveInformation::class.java,
)
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> = moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> by lazy {
moshi.adapter(UserWalletSensitiveInformation::class.java)
}
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> by lazy {
moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java),
)
}
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
if (encryptionKey == null) {

View file

@ -15,7 +15,6 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
get() = UserWalletPublicInformation(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
isMultiCurrency = isMultiCurrency,
scanResponse = scanResponse.copy(
@ -31,7 +30,6 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
return UserWallet(
name = name,
walletId = walletId,
artworkUrl = artworkUrl,
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,

View file

@ -128,6 +128,8 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = runCatching {
// TODO [REDACTED_TASK_KEY]
error("sign and get specific error to switch to card_id flow")
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
}.getOrElse {
Timber.i(
@ -180,6 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
@Suppress("LongMethod")
private suspend fun SessionContext.handleCardAuthorization(
cardWalletAddress: String,
): CompletionResult<VisaCardActivationStatus> {
@ -232,11 +235,17 @@ internal class VisaCardScanHandler @Inject constructor(
tokens = authorizationTokensResponse,
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
val activationRemoteState = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
}
val error = when (activationRemoteState) {
VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation
VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState
VisaActivationRemoteState.Failed -> VisaActivationError.FailedRemoteState
else -> null
}

View file

@ -4,8 +4,8 @@ import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.*
import com.tangem.blockchainsdk.utils.fromNetworkId
@ -16,6 +16,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@ -101,7 +102,7 @@ class WalletConnectSdkHelper {
sourceAddress = transaction.from,
destinationAddress = destinationAddress,
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()),
gasLimit = gasLimit.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
@ -191,7 +192,7 @@ class WalletConnectSdkHelper {
val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
amount = Amount(value, walletManager.wallet.blockchain),
destination = transaction.to ?: "",
data = transaction.data,
callData = CompiledSmartContractCallData(transaction.data.hexToBytes()),
)
return when (gasLimitResult) {
is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2"))
@ -357,36 +358,9 @@ class WalletConnectSdkHelper {
)
}
private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
message.data.asciiToHex()?.hexToBytes() ?: byteArrayOf()
}
private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data)
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
return (prefixData + messageData).toKeccak()
}
private fun String.hexToAscii(): String? {
return try {
removePrefix(HEX_PREFIX).hexToBytes()
.map {
val char = it.toInt().toChar()
if (char.isAscii()) char else return null
}
.joinToString("")
} catch (exception: Exception) {
return null
}
}
private fun String.asciiToHex(): String? {
return map {
if (!it.isAscii()) return null
Integer.toHexString(it.code)
}.joinToString("")
}
private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this)
suspend fun signPersonalMessage(
hashToSign: ByteArray,
@ -415,11 +389,11 @@ class WalletConnectSdkHelper {
return when (type) {
WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
else -> EthereumUtils.prepareSignedMessageData(
signedHash = signedHash,
hashToSign = hashToSign,
else -> UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,
hash = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
).asRSVLegacyEVM().toHexString()
}
}
is CompletionResult.Failure -> {
@ -535,7 +509,6 @@ class WalletConnectSdkHelper {
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
// TODO remove after [REDACTED_JIRA]

View file

@ -10,6 +10,8 @@ import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.unsupportedDApps
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -21,8 +23,99 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.O
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import timber.log.Timber
internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
private val stub: LegacyWalletConnectRepositoryStub,
private val legacy: DefaultLegacyWalletConnectRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val wcInitializeUseCase: WcInitializeUseCase,
) : LegacyWalletConnectRepository {
private val isNewWc by lazy {
walletConnectFeatureToggles.isRedesignedWalletConnectEnabled
}
override val events: Flow<WalletConnectEvents> by lazy {
if (isNewWc) stub.events else legacy.events
}
override val activeSessions: Flow<List<WalletConnectSession>> by lazy {
if (isNewWc) stub.activeSessions else legacy.activeSessions
}
override val currentSessions: List<WalletConnectSession> by lazy {
if (isNewWc) stub.currentSessions else legacy.currentSessions
}
override fun init(projectId: String) {
if (isNewWc) {
wcInitializeUseCase.init(projectId)
} else {
legacy.init(projectId)
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
if (isNewWc) stub.setUserNamespaces(userNamespaces) else legacy.setUserNamespaces(userNamespaces)
}
override fun updateSessions() {
if (isNewWc) stub.updateSessions() else legacy.updateSessions()
}
override fun pair(uri: String, source: SourceType) {
if (isNewWc) stub.pair(uri, source) else legacy.pair(uri, source)
}
override fun disconnect(topic: String) {
if (isNewWc) stub.disconnect(topic) else legacy.disconnect(topic)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
if (isNewWc) stub.approve(userNamespaces, blockchainNames) else legacy.approve(userNamespaces, blockchainNames)
}
override fun reject() {
if (isNewWc) stub.reject() else legacy.reject()
}
override fun sendRequest(requestData: RequestData, result: String) {
if (isNewWc) stub.sendRequest(requestData, result) else legacy.sendRequest(requestData, result)
}
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
if (isNewWc) stub.rejectRequest(requestData, error) else legacy.rejectRequest(requestData, error)
}
override fun cancelRequest(topic: String, id: Long, message: String) {
if (isNewWc) stub.cancelRequest(topic, id, message) else legacy.cancelRequest(topic, id, message)
}
}
internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository {
override val events: Flow<WalletConnectEvents> = emptyFlow()
override val activeSessions: Flow<List<WalletConnectSession>> = emptyFlow()
override val currentSessions: List<WalletConnectSession> = listOf()
override fun init(projectId: String) = Unit
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) = Unit
override fun updateSessions() = Unit
override fun pair(uri: String, source: SourceType) = Unit
override fun disconnect(topic: String) = Unit
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) = Unit
override fun reject() = Unit
override fun sendRequest(requestData: RequestData, result: String) = Unit
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) = Unit
override fun cancelRequest(topic: String, id: Long, message: String) = Unit
}
@Suppress("LargeClass")
internal class DefaultLegacyWalletConnectRepository(
private val application: Application,

View file

@ -8,16 +8,21 @@ import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepositoryFacade
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.data.LegacyWalletConnectRepositoryStub
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.tap.domain.walletconnect2.toggles.DefaultWalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -60,7 +65,7 @@ internal object WalletConnectModule {
@Provides
@Singleton
fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
return WalletConnectFeatureToggles(featureTogglesManager)
return DefaultWalletConnectFeatureToggles(featureTogglesManager)
}
@Provides
@ -69,12 +74,21 @@ internal object WalletConnectModule {
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
wcInitializeUseCase: WcInitializeUseCase,
): LegacyWalletConnectRepository {
return DefaultLegacyWalletConnectRepository(
val legacy = DefaultLegacyWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
)
val stub = LegacyWalletConnectRepositoryStub()
return DefaultLegacyWalletConnectRepositoryFacade(
stub,
legacy,
walletConnectFeatureToggles,
wcInitializeUseCase,
)
}
@Provides

View file

@ -1,11 +1,12 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
internal class WalletConnectFeatureToggles(
internal class DefaultWalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
) : WalletConnectFeatureToggles {
val isRedesignedWalletConnectEnabled: Boolean
override val isRedesignedWalletConnectEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED")
}

View file

@ -1,87 +0,0 @@
package com.tangem.tap.features
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackPressedHandler {
private lateinit var mainView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureTransitions()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mainView = super.onCreateView(inflater, container, savedInstanceState)!!
return mainView
}
override fun handleOnBackPressed() {
store.dispatchNavigationAction(AppRouter::pop)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadToolbarMenu()?.let {
val menuHost = requireActivity() as? MenuHost ?: return
menuHost.addMenuProvider(it, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
}
protected open fun loadToolbarMenu(): MenuProvider? = null
protected open fun configureTransitions() {
configureDefaultTransactions()
}
protected fun configureDefaultTransactions() {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
fun showRetrySnackbar(message: String, action: VoidCallback) {
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(getString(R.string.common_retry)) {
snackbar.dismiss()
action()
}.show()
}
}
interface FragmentOnBackPressedHandler {
fun handleOnBackPressed()
}
@SuppressLint("FragmentBackPressedCallback")
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
requireActivity().onBackPressedDispatcher.addCallback(
owner = this,
onBackPressed = { handler.handleOnBackPressed() },
)
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener {
handler.handleOnBackPressed()
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : BaseFragment(layoutId) {
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
abstract fun subscribeToStore()
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
@ -16,13 +15,8 @@ import org.rekotlin.Action
object DemoHelper {
val config = DemoConfig()
private val demoMiddlewares = listOf(
DemoOnboardingNoteMiddleware(),
)
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
@ -35,10 +29,6 @@ object DemoHelper {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
demoMiddlewares.forEach {
if (it.tryHandle(config, scanResponse, action)) return true
}
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender)
uiMessageSender.send(

View file

@ -1,65 +0,0 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
internal class DemoOnboardingNoteMiddleware : DemoMiddleware {
override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean {
val noteState = store.state.onboardingNoteState
return when (action) {
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard {
return false
}
store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val balanceAmount = config.getBalance(walletManager.wallet.blockchain)
val loadedBalance = noteState.walletBalance.copy(
value = balanceAmount.value!!,
currency = Currency.Blockchain(walletManager.wallet.blockchain, null),
state = ProgressState.Done,
error = null,
criticalError = null,
)
walletManager.wallet.setAmount(balanceAmount)
scope.launch {
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
return true
}
else -> false
}
}
}

View file

@ -20,7 +20,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -33,8 +32,6 @@ import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.R
@ -56,7 +53,6 @@ internal class CardSettingsModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
@ -200,23 +196,13 @@ internal class CardSettingsModel @Inject constructor(
}
if (scanResponse.cardTypesResolver.isTangemTwins()) {
if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) {
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.RecreateWalletTwin,
),
)
}
} else {
// needs to prepare twin state if it's twin cards (depends on onboarding refactoring)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
cardSettingsInteractor.clear()
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.RecreateWalletTwin,
),
)
}
} else {
val card = scanResponse.card

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
@ -29,8 +28,6 @@ internal fun SettingsScreensScaffold(
) {
val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {

View file

@ -2,14 +2,18 @@ package com.tangem.tap.features.home
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.LocalRootBackgroundColor
import com.tangem.core.ui.utils.findActivity
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
@ -61,6 +65,17 @@ internal class DefaultHomeComponent @AssistedInject constructor(
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
val rootBackgroundColor = LocalRootBackgroundColor.current
val previousColor = remember { rootBackgroundColor.value }
val storiesBackgroundColor = Color(color = 0xFF010101)
DisposableEffect(Unit) {
rootBackgroundColor.value = storiesBackgroundColor
onDispose {
rootBackgroundColor.value = previousColor
}
}
}
override fun newState(state: HomeState) {

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.ui.R
import com.tangem.core.ui.coil.ImagePreloader
import com.tangem.core.ui.extensions.resourceReference
@ -21,6 +22,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
@ -30,7 +32,11 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.features.onramp.deeplink.OnrampDeepLink
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
@ -58,6 +64,9 @@ internal class MainViewModel @Inject constructor(
private val getStoryContentUseCase: GetStoryContentUseCase,
private val imagePreloader: ImagePreloader,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
private val onboardingRepository: OnboardingRepository,
private val deepLinksRegistry: DeepLinksRegistry,
private val onrampDeepLinkFactory: OnrampDeepLink.Factory,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -92,6 +101,15 @@ internal class MainViewModel @Inject constructor(
sendKeyboardIdentifierEvent()
preloadImages()
initializeDeepLinks()
}
fun checkForUnfinishedBackup() {
viewModelScope.launch(dispatchers.main) {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
}
}
/** Loading the resources needed to run the application */
@ -321,4 +339,8 @@ internal class MainViewModel @Inject constructor(
)
}
}
private fun initializeDeepLinks() {
deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope))
}
}

View file

@ -1,13 +1,10 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.redux.StateDialog
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingDialog : StateDialog {
object TwinningProcessNotCompleted : OnboardingDialog()
data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog()
data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog()
}

View file

@ -1,46 +1,23 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure
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.Basic
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
object OnboardingHelper {
suspend fun isOnboardingCase(response: ScanResponse): Boolean {
val onboardingManager =
store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isVisaWallet() -> {
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
@ -52,227 +29,20 @@ object OnboardingHelper {
if (!response.twinsIsTwinned()) {
true
} else {
onboardingManager.isActivationInProgress(cardId)
cardRepository.isActivationInProgress(cardId)
}
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = onboardingManager.isActivationInProgress(cardId)
val activationInProgress = cardRepository.isActivationInProgress(cardId)
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress || isNoBackup
}
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId)
response.card.wallets.isNotEmpty() -> cardRepository.isActivationInProgress(cardId)
else -> true
}
}
@Suppress("CyclomaticComplexMethod")
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
scanResponse.productType == ProductType.Wallet ||
scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported
val featureToggles = store.inject(DaggerGraphState::onboardingV2FeatureToggles)
if (featureToggles.isOnboardingV2Enabled &&
newOnboardingSupportTypes
) {
val backupState = store.state.onboardingWalletState.backupState
return AppRoute.Onboarding(
scanResponse = scanResponse,
mode = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1
},
)
}
return when (val type = scanResponse.productType) {
ProductType.Note -> if (featureToggles.isNoteRefactoringEnabled) {
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingNote
}
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> if (scanResponse.card.settings.isBackupAllowed) {
AppRoute.OnboardingWallet()
} else {
AppRoute.OnboardingOther
}
ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) {
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingTwins
}
ProductType.Start2Coin,
ProductType.Visa,
-> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported")
}
}
suspend fun saveWallet(
alreadyCreatedWallet: UserWallet?,
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = false,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
}
fun trySaveWalletAndNavigateToWalletScreen(
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = true,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
delay(timeMillis = 1_800)
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
}
}
}
}
fun onInterrupted() {
Analytics.removeContext()
}
fun sendToppedUpEvent(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
if (userWalletId != null && currency != null) {
Analytics.send(Basic.ToppedUp(userWalletId.stringValue, currency))
}
}
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) {
val blockchain = walletManager.wallet.blockchain
val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains)
val cryptoCurrency = CryptoCurrencyFactory(excludedBlockchains).createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
}
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
alreadyCreatedWallet: UserWallet? = null,
) {
val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory)
val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {
Timber.e("User wallet not created")
return
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
mainScope.launch { store.onUserWalletSelected(userWallet) }
store.dispatchWithMain(OnboardingWalletAction.WalletSaved(userWallet.walletId))
}
}
}

View file

@ -1,141 +1,6 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.hasPendingTransactions
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.isPositive
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Remove when navigation refactoring will be implemented")
class OnboardingManager(var scanResponse: ScanResponse) {
private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) }
private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) }
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
val cardInfo = cardInfo
?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
this.cardInfo = cardInfo
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier)
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
val isDemoCard = scanResponse.isDemoCard()
val balance = when (val result = walletManager.safeUpdate(isDemoCard)) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, wallet.hasPendingTransactions())
} else {
OnboardingWalletBalance.done(valueOfAmount, false)
}
balance
}
}
is Result.Failure -> {
val error = result.error as? TapError ?: TapError.UnknownError
when (error) {
is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManager.InternalError
else -> {
Timber.e(error.localizedMessage)
OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater)
}
}
}
}
return balance.copy(
currency = Currency.Blockchain(
blockchain = walletManager.wallet.blockchain,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath,
),
)
}
suspend fun startActivation(cardId: String) {
cardRepository.startCardActivation(cardId)
}
suspend fun finishActivation(cardId: String) {
cardRepository.finishCardActivation(cardId)
}
suspend fun finishActivation(cardIds: List<String>) {
cardRepository.finishCardsActivation(cardIds)
}
suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId)
suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId)
suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId)
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency = Currency.Blockchain(Blockchain.Unknown, null),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error,
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error,
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading,
)
fun done(value: BigDecimal, hasTransactions: Boolean): OnboardingWalletBalance = OnboardingWalletBalance(
value,
hasIncomingTransaction = hasTransactions,
state = ProgressState.Done,
)
}
}
class OnboardingManager(var scanResponse: ScanResponse)

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.onboarding
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
class OnboardingMenuProvider(
private val scanResponseProvider: Provider<ScanResponse>,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_onboarding, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponseProvider()).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
true
}
else -> false
}
}

View file

@ -1,54 +0,0 @@
package com.tangem.tap.features.onboarding.products
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
protected val binding: FragmentOnboardingMainBinding by viewBinding(FragmentOnboardingMainBinding::bind)
protected val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
override fun configureTransitions() {
enterTransition = HomeToOnboardingTransition()
exitTransition = HomeToOnboardingTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lavConfetti.playAnimation()
} else {
lavConfetti.cancelAnimation()
}
}
}

View file

@ -1,228 +0,0 @@
package com.tangem.tap.features.onboarding.products.note
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
import com.tangem.tap.store
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.wallet.R
@Deprecated("Use OnboardingNoteComponent")
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None
private val mainBinding by lazy { binding.vMain }
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
binding.toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
store.dispatch(OnboardingNoteAction.Init)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingNoteState == newState.onboardingNoteState
}.select { it.onboardingNoteState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingNoteState) {
if (activity == null || view == null) return
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingNoteStep.None -> setupNoneState()
OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state)
OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state)
OnboardingNoteStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
private fun setupNoneState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.isVisible = false
btnAlternativeAction.isVisible = false
btnRefreshBalanceWidget.show(false)
tvHeader.isVisible = false
tvBody.isVisible = false
btnMainAction.text = ""
btnMainAction.icon = null
btnAlternativeAction.text = ""
btnAlternativeAction.icon = null
tvHeader.text = ""
tvBody.text = ""
}
private fun setupCreateWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingNoteAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle),
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
btnAlternativeAction.isVisible = false // temporary
}
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
if (availableForBuy(state.scanResponse, state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
btnAlternativeAction.isVisible = false
}
tvHeader.setText(R.string.onboarding_topup_title)
if (state.balanceNonCriticalError == null) {
tvBody.setText(R.string.onboarding_top_up_body)
} else {
state.walletBalance.amountToCreateAccount?.let { amount ->
val tvBodyMessage = getString(
R.string.onboarding_top_up_body_no_account_error,
amount,
state.walletBalance.currency.currencySymbol,
)
tvBody.text = tvBodyMessage
}
}
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet)
}
private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.text = ""
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation)
}
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
scanResponse ?: return false
return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingNoteAction.OnBackPressed)
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingNoteAction : Action {
// from user, ui
object Init : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()
object ShowAddressInfoDialog : OnboardingNoteAction()
object OnBackPressed : OnboardingNoteAction()
// from redux
class SetArtworkUrl(val artworkUrl: String) : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()
data class SetStepOfScreen(val step: OnboardingNoteStep) : OnboardingNoteAction()
sealed class Balance {
object Update : OnboardingNoteAction()
data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction()
data class SetCriticalError(val error: TapError?) : OnboardingNoteAction()
data class SetNonCriticalError(val error: TapError?) : OnboardingNoteAction()
}
sealed class Confetti {
object Show : OnboardingNoteAction()
object Hide : OnboardingNoteAction()
}
}

View file

@ -1,197 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
object OnboardingNoteMiddleware {
val handler = onboardingNoteMiddleware
}
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(state, action, dispatch)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
if (DemoHelper.tryHandle(appState, action)) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val scanResponse = onboardingManager.scanResponse
val card = onboardingManager.scanResponse.card
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.Init -> {
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artworkUrl)) }
}
}
is OnboardingNoteAction.DetermineStepOfScreen -> {
val step = when {
card.wallets.isEmpty() -> OnboardingNoteStep.CreateWallet
noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done
else -> OnboardingNoteStep.TopUpWallet
}
store.dispatch(OnboardingNoteAction.SetStepOfScreen(step))
}
is OnboardingNoteAction.SetStepOfScreen -> {
when (action.step) {
OnboardingNoteStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingNoteStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(OnboardingNoteAction.Balance.Update)
}
OnboardingNoteStep.Done -> {
Analytics.send(Onboarding.Finished())
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
else -> Unit
}
}
is OnboardingNoteAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> Unit
}
}
}
}
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
),
state = ProgressState.Loading,
error = null,
criticalError = null,
)
store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager.updateBalance(walletManager)
delay(if (isLoadedBefore) 0 else 300)
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(scanResponse)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
Analytics.send(Onboarding.Topup.ButtonShowWalletAddress())
val appDialog = AppDialog.AddressInfoDialog(noteState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val walletManager = noteState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = scanResponse,
)
}
is OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
is OnboardingNoteAction.OnBackPressed -> {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatchNavigationAction(AppRouter::pop)
},
),
)
}
else -> Unit
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingNoteReducer {
fun reduce(action: Action, state: AppState): OnboardingNoteState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): OnboardingNoteState {
var state = appState.onboardingNoteState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingNoteState(scanResponse = action.scanResponse)
}
is OnboardingNoteAction.SetArtworkUrl -> {
state = state.copy(cardArtworkUrl = action.artworkUrl)
}
is OnboardingNoteAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is OnboardingNoteAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is OnboardingNoteAction.Balance.SetCriticalError -> {
state = state.copy(balanceCriticalError = action.error)
}
is OnboardingNoteAction.Balance.SetNonCriticalError -> {
state = state.copy(balanceNonCriticalError = action.error)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingNoteAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingNoteAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
// UI
val cardArtworkUrl: String? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
val scanResponse: ScanResponse? = null,
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsAction
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
private val mainBinding by lazy { binding.vMain }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
binding.toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.Init)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingOtherCardsState == newState.onboardingOtherCardsState
}.select { it.onboardingOtherCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingOtherCardsState) {
if (activity == null || view == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState()
OnboardingOtherCardsStep.Done -> setupDoneState()
else -> {}
}
showConfetti(state.showConfetti)
}
private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingOtherCardsAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
btnAlternativeAction.isVisible = false // temporary
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle),
)
updateConstraints(R.layout.lp_onboarding_create_wallet)
}
private fun setupDoneState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingOtherCardsAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.text = ""
btnAlternativeAction.setOnClickListener { }
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
updateConstraints(R.layout.lp_onboarding_done)
}
private fun updateConstraints(@LayoutRes layoutId: Int) = with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingOtherCardsAction.OnBackPressed)
super.handleOnBackPressed()
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingOtherCardsAction : Action {
// from user, ui
object Init : OnboardingOtherCardsAction()
object LoadCardArtwork : OnboardingOtherCardsAction()
object CreateWallet : OnboardingOtherCardsAction()
// from redux
class SetArtworkUrl(val artworkUrl: String) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()
object OnBackPressed : OnboardingOtherCardsAction()
data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction()
sealed class Confetti {
object Show : OnboardingOtherCardsAction()
object Hide : OnboardingOtherCardsAction()
}
}

View file

@ -1,129 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
object OnboardingOtherCardsMiddleware {
val handler = onboardingOtherCardsMiddleware
}
private val onboardingOtherCardsMiddleware: Middleware<AppState> = { dispatch, _ ->
{ next ->
{ action ->
handleOtherCardsAction(action)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleOtherCardsAction(action: Action) {
if (action !is OnboardingOtherCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val card = onboardingManager.scanResponse.card
when (action) {
is OnboardingOtherCardsAction.Init -> {
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artworkUrl)) }
}
}
is OnboardingOtherCardsAction.DetermineStepOfScreen -> {
val step = when {
card.wallets.isEmpty() -> OnboardingOtherCardsStep.CreateWallet
else -> OnboardingOtherCardsStep.Done
}
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(step))
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
when (action.step) {
OnboardingOtherCardsStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingOtherCardsStep.Done -> {
Analytics.send(Onboarding.Finished())
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
else -> Unit
}
}
is OnboardingOtherCardsAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(
scanResponse = onboardingManager.scanResponse,
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = onboardingManager.scanResponse.copy(
card = result.data.card,
)
val updatedCard = updatedResponse.card
onboardingManager.scanResponse = updatedResponse
onboardingManager.startActivation(updatedCard.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
}
}
}
}
}
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
}
OnboardingOtherCardsAction.OnBackPressed -> {
OnboardingHelper.onInterrupted()
}
else -> Unit
}
}
private fun handleActivationError() {
store.dispatchDialogShow(
OnboardingDialog.WalletActivationError(
onConfirm = {
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true))
},
),
)
}

View file

@ -1,35 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingOtherCardsReducer {
fun reduce(action: Action, state: AppState): OnboardingOtherCardsState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): OnboardingOtherCardsState {
var state = appState.onboardingOtherCardsState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingOtherCardsState()
}
is OnboardingOtherCardsAction.SetArtworkUrl -> {
state = state.copy(cardArtworkUrl = action.artworkUrl)
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingOtherCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingOtherCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingOtherCardsState(
// UI
val cardArtworkUrl: String? = null,
val showConfetti: Boolean = false,
val currentStep: OnboardingOtherCardsStep = OnboardingOtherCardsStep.None,
val steps: List<OnboardingOtherCardsStep> = OnboardingOtherCardsStep.values().toList(),
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingOtherCardsStep {
None, CreateWallet, Done
}

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class TwinCardsAction : Action {
data class IfTwinsPrepareState(val scanResponse: ScanResponse) : TwinCardsAction()
object Init : TwinCardsAction()
data class SetMode(val mode: CreateTwinWalletMode) : TwinCardsAction()
data class SetStepOfScreen(val step: TwinCardsStep) : TwinCardsAction()
data class SetUserUnderstand(val isUnderstand: Boolean) : TwinCardsAction()
sealed class CardsManager {
data class Set(val manager: TwinCardsManager) : TwinCardsAction()
object Release : TwinCardsAction()
}
sealed class Wallet : TwinCardsAction() {
data class LaunchFirstStep(val initialMessage: Message) : TwinCardsAction()
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : TwinCardsAction()
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
}
// for the onboarding
data class SetPairCardId(val cardId: String) : TwinCardsAction()
object TopUp : TwinCardsAction()
object ShowAddressInfoDialog : TwinCardsAction()
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
object Done : TwinCardsAction()
data class OnBackPressed(
// this is necessary for the correct animation of the return of cards to the Home screen
val shouldResetTwinCardsWidget: (should: Boolean, popAction: VoidCallback) -> Unit,
) : TwinCardsAction()
data class SaveScannedTwinCardAndNavigateToWallet(
val scanResponse: ScanResponse,
) : TwinCardsAction()
sealed class Balance {
object Update : TwinCardsAction()
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()
data class SetCriticalError(val error: TapError?) : TwinCardsAction()
data class SetNonCriticalError(val error: TapError?) : TwinCardsAction()
}
sealed class Confetti {
object Show : TwinCardsAction()
object Hide : TwinCardsAction()
}
}

View file

@ -1,399 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import kotlin.reflect.KClass
object TwinCardsMiddleware {
val handler = twinsWalletMiddleware
}
private val twinsWalletMiddleware: Middleware<AppState> = { dispatch, _ ->
{ next ->
{ action ->
handle(action, dispatch)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handle(action: Action, dispatch: DispatchFunction) {
if (action !is TwinCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val twinCardsState = store.state.twinCardsState
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
fun getScanResponse(): ScanResponse {
return when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
} ?: throw NullPointerException("ScanResponse can't be NULL")
}
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response))
}
}
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
scope.launch {
onboardingManager?.startActivation(cardId)
}
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
Analytics.send(Onboarding.Finished())
val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId)
if (cardIds.isNotEmpty()) {
mainScope.launch {
onboardingManager?.finishActivation(cardIds)
}
}
}
}
when (action) {
is TwinCardsAction.Init -> {
mainScope.launch {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch
if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) {
store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse))
}
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
mainScope.launch {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
val dispatchAction = if (wasTwinsOnboardingShown) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
TwinCardsAction.SetStepOfScreen(step)
} else {
TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome)
}
store.dispatch(dispatchAction)
}
}
is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
scope.launch {
store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke()
}
}
is TwinCardsStep.CreateFirstWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
Analytics.send(Onboarding.Twins.SetupStarted())
}
TwinCardsStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(TwinCardsAction.Balance.Update)
}
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
TwinCardsStep.None,
TwinCardsStep.Warning,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
-> Unit
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val manager = TwinCardsManager(card = getScanResponse().card)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
when (val result = manager.createFirstWallet(action.initialMessage)) {
is CompletionResult.Success -> {
// remove wallet only after first step of retwin
scope.launch {
userWalletsListManager.delete(
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
)
}
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet))
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Wallet.LaunchSecondStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.createSecondWallet(
action.initialMessage,
action.preparingMessage,
action.creatingWalletMessage,
)
when (result) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetPairCardId(result.data.cardId))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet))
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
when (val result = manager.complete(action.message)) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.Twins.SetupFinished())
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet))
}
is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Balance.Update -> {
val walletManager = if (twinCardsState.walletManager != null) {
twinCardsState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(getScanResponse()).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(TwinCardsAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
),
state = ProgressState.Loading,
error = null,
criticalError = null,
)
store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager?.updateBalance(walletManager) ?: return@launch
loadedBalance.criticalError?.let(store::dispatchErrorNotification)
delay(if (isLoadedBefore) 0 else 300)
withMainContext {
store.dispatch(TwinCardsAction.Balance.Set(loadedBalance))
store.dispatch(TwinCardsAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(TwinCardsAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(getScanResponse())
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
is TwinCardsAction.ShowAddressInfoDialog -> {
val addressData = twinCardsState.walletManager?.getAddressData() ?: return
val appDialog = AppDialog.AddressInfoDialog(twinCardsState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val walletManager = twinCardsState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = onboardingManager?.scanResponse ?: return,
)
}
TwinCardsAction.Done -> {
val scanResponse = getScanResponse()
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
is CreateTwinWalletMode.RecreateWallet -> {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
} else {
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}
}
}
}
is TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet -> {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = action.scanResponse,
)
}
is TwinCardsAction.OnBackPressed -> {
if (twinCardsState.twinningInProgress) {
store.dispatchDialogShow(OnboardingDialog.TwinningProcessNotCompleted)
} else {
val onOkCallback = {
val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet &&
twinCardsState.currentStep != TwinCardsStep.TopUpWallet &&
twinCardsState.currentStep != TwinCardsStep.Done
OnboardingHelper.onInterrupted()
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) }
}
}
store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback))
}
}
else -> Unit
}
}
private fun showCardVerificationFailedDialog(error: TangemError) {
if (error is TangemSdkError.CardVerificationFailed) {
Analytics.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
AnalyticsParam.ScreensSources.Onboarding,
),
)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
type = FeedbackEmailType.CardAttestationFailed,
)
}
},
),
)
}
}
private fun getPopBackScreen(): KClass<out AppRoute> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
return if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
if (isLocked) {
AppRoute.Welcome::class
} else {
AppRoute.Wallet::class
}
} else {
AppRoute.Home::class
}
}

View file

@ -1,77 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object TwinCardsReducer {
fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state)
}
@Suppress("ComplexMethod")
private fun internalReduce(action: Action, state: AppState): TwinCardsState {
if (action !is TwinCardsAction) return state.twinCardsState
var state = state.twinCardsState
when (action) {
is TwinCardsAction.IfTwinsPrepareState -> {
state = if (action.scanResponse.cardTypesResolver.isTangemTwins()) {
val cardNumber = action.scanResponse.card.getTwinCardNumber() ?: return state
TwinCardsState(
cardNumber = cardNumber,
mode = CreateTwinWalletMode.CreateWallet,
)
} else {
TwinCardsState()
}
}
is TwinCardsAction.SetMode -> {
state = state.copy(
mode = action.mode,
userWasUnderstandIfWalletRecreate = false,
)
}
is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(
currentStep = action.step,
welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse,
)
}
is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)
}
is TwinCardsAction.CardsManager.Set -> {
state = state.copy(twinCardsManager = action.manager)
}
TwinCardsAction.CardsManager.Release -> {
state = state.copy(twinCardsManager = null)
}
is TwinCardsAction.SetPairCardId -> {
state = state.copy(pairCardId = action.cardId)
}
is TwinCardsAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is TwinCardsAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is TwinCardsAction.Balance.SetCriticalError -> {
state = state.copy(balanceCriticalError = action.error)
}
is TwinCardsAction.Balance.SetNonCriticalError -> {
state = state.copy(balanceNonCriticalError = action.error)
}
is TwinCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is TwinCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
else -> {}
}
return state
}

View file

@ -1,82 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class TwinCardsState(
// if cardNumber = null -> the scanned card is not Twin
val cardNumber: TwinCardNumber? = null,
val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet,
val currentStep: TwinCardsStep = TwinCardsStep.Welcome,
val userWasUnderstandIfWalletRecreate: Boolean = false,
// available only when creating or recreating wallets
val twinCardsManager: TwinCardsManager? = null,
// for the onboarding
val pairCardId: String? = null, // available after create\recreate wallets
val walletManager: WalletManager? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val showConfetti: Boolean = false,
val welcomeOnlyScanResponse: ScanResponse? = null,
) : StateType {
val steps: List<TwinCardsStep>
get() = when (mode) {
is CreateTwinWalletMode.CreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.TopUpWallet,
TwinCardsStep.Done,
)
is CreateTwinWalletMode.RecreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.Done,
)
}
val progress: Int
get() = steps.indexOf(currentStep)
val twinningInProgress: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
}
sealed class CreateTwinWalletMode {
data object CreateWallet : CreateTwinWalletMode()
data class RecreateWallet(
val scanResponse: ScanResponse,
) : CreateTwinWalletMode()
}
sealed class TwinCardsStep {
object None : TwinCardsStep()
data class WelcomeOnly(
val scanResponse: ScanResponse,
) : TwinCardsStep()
object Welcome : TwinCardsStep()
object Warning : TwinCardsStep()
object CreateFirstWallet : TwinCardsStep()
object CreateSecondWallet : TwinCardsStep()
object CreateThirdWallet : TwinCardsStep()
// for the onboarding
object TopUpWallet : TwinCardsStep()
object Done : TwinCardsStep()
}

View file

@ -1,460 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.Artwork
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
@Suppress("LargeClass")
@AndroidEntryPoint
internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>() {
private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(TwinCardsAction.Init)
}
override fun configureTransitions() {
when (store.state.twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
super.configureTransitions()
}
is CreateTwinWalletMode.RecreateWallet -> {
configureDefaultTransactions()
}
}
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider { getActualScanResponse() },
)
@Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
reconfigureLayoutForTwins(mainBinding.onboardingTopContainer)
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(
LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer),
deviceScaleFactorForWelcomeState,
) {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget =
RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1_URL) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2_URL) {
placeholder(R.drawable.card_placeholder_white)
error(R.drawable.card_placeholder_white)
fallback(R.drawable.card_placeholder_white)
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_primary)
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
with(containerBinding) {
imvFrontCard.hide()
imvCardBackground.hide()
cardsContainer.show()
imvTwinFrontCard.transitionName = ShareElement.imvFrontCard
imvTwinBackCard.transitionName = ShareElement.imvBackCard
// if don't this, the bg_circle_... is overflow the app_bar
binding.appBar.bringToFront()
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.twinCardsState == newState.twinCardsState
}.select { it.twinCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: TwinCardsState) {
if (activity == null || view == null) return
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
is TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state, state.currentStep.scanResponse)
is TwinCardsStep.Welcome -> setupWelcomeState(state)
is TwinCardsStep.Warning -> setupWarningState(state)
is TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
is TwinCardsStep.CreateSecondWallet -> setupCreateSecondWalletState(state)
is TwinCardsStep.CreateThirdWallet -> setupCreateThirdWalletState(state)
is TwinCardsStep.TopUpWallet -> setupTopUpWalletState(state)
is TwinCardsStep.Done -> setupDoneState(state)
else -> {}
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: TwinCardsState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
private fun setupWelcomeOnlyState(state: TwinCardsState, scanResponse: ScanResponse) {
setupWelcomeState(state) {
store.dispatch(TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet(scanResponse))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None))
}
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupWelcomeState(state: TwinCardsState, onMainButtonClick: VoidCallback) =
with(mainBinding.onboardingActionContainer) {
twinsWidget.toWelcome(false)
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show()
pbBinding.pbState.hide()
tvHeader.setText(R.string.twins_onboarding_subtitle)
tvBody.text = getString(
R.string.twins_onboarding_description_format,
state.cardNumber?.pairIndexNumber(),
)
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener { onMainButtonClick() }
}
private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
twinsWidget.toWelcome(false)
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
pbBinding.pbState.hide()
chbUnderstand.show()
tvHeader.setText(R.string.common_warning)
tvBody.setText(R.string.twins_recreate_warning)
chbUnderstand.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide()
chbUnderstand.hide()
pbBinding.pbState.show()
mainBinding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> {}
TwinCardNumber.Second -> {
switchToCard(
state.cardNumber,
false,
)
}
else -> {}
}
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
}
else -> {}
}
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
),
)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text =
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchSecondStep(
Message(
getString(
R.string.twins_recreate_title_format,
twinPairIndexNumber,
),
),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
),
)
}
}
private fun setupCreateThirdWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
switchToCard(state.cardNumber)
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
),
)
}
}
private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
when (previousStep) {
TwinCardsStep.None, TwinCardsStep.Welcome -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(true) {
twinsWidget.toActivate(true)
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, true) {
twinsWidget.toActivate(true)
}
}
else -> {}
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
else -> {}
}
if (availableForBuy(getActualScanResponse(), state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
btnAlternativeAction.isVisible = false
}
tvHeader.setText(R.string.onboarding_topup_title)
tvBody.setText(R.string.onboarding_top_up_body)
btnMainAction.icon = null
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
}
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList =
requireContext().resources.getColorStateList(R.color.onboarding_card_background, null)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
val position = cardNumber?.number ?: return
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {
twinsWidget.leapfrogWidget.leap(animate, onEnd)
} else {
onEnd()
}
}
private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
scanResponse ?: return false
return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
}
private fun getActualScanResponse(): ScanResponse {
return store.state.twinCardsState.welcomeOnlyScanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
}
override fun handleOnBackPressed() {
store.dispatch(
TwinCardsAction.OnBackPressed { should, popAction ->
store.dispatch(TwinCardsAction.Confetti.Hide)
showConfetti(false)
if (should) {
switchToCard(TwinCardNumber.First, true, popAction)
} else {
popAction()
}
},
)
}
}
private fun TwinCardNumber?.indexNumber(): String {
return this?.number?.toString() ?: ""
}
private fun TwinCardNumber?.pairIndexNumber(): String {
return this?.pairNumber()?.number?.toString() ?: ""
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.ui.dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object TwinningProcessNotCompletedDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog)
.setMessage(R.string.onboarding_twin_exit_warning)
.setPositiveButton(R.string.warning_button_ok) { _, _ -> }
.setOnDismissListener { store.dispatchDialogHide() }
.create()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
sealed class BackupDialog : StateDialog {
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Onboarding.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
when (action) {
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.discardSavedBackup()
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Finished())
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
}
}
}
}

View file

@ -1,108 +1,19 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
data object Init : OnboardingWalletAction()
data object GetToCreateWalletStep : OnboardingWalletAction()
data object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val shouldSendAnalyticsEvent: Boolean,
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
data object Done : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetPrimaryCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetSecondCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
data object OnBackPressed : OnboardingWalletAction()
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
data class Init(val maxProgress: Int) : OnboardingWallet2Action()
data class SetDependencies(val maxProgress: Int) : OnboardingWallet2Action()
data class CreateWallet(
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class ImportWallet(
val mnemonicComponents: List<String>,
val passphrase: String?,
val seedPhraseSource: SeedPhraseSource,
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class WalletWasCreated(
val result: CompletionResult<CreateWalletResponse>,
) : OnboardingWallet2Action()
}
sealed class BackupAction : Action {
data object IntroduceBackup : BackupAction()
data object StartBackup : BackupAction()
data object SkipBackup : BackupAction()
data object ErrorInBackupCard : BackupAction()
data object StartAddingPrimaryCard : BackupAction()
data object ScanPrimaryCard : BackupAction()
data class SetHasRing(val hasRing: Boolean) : BackupAction()
/**
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data object CheckForUnfinishedBackup : BackupAction()
data object StartAddingBackupCards : BackupAction()
data object AddBackupCard : BackupAction() {
data class Success(val card: Card) : BackupAction()
data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction()
}
data object FinishAddingBackupCards : BackupAction()
data object ShowAccessCodeInfoScreen : BackupAction()
data object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
data object OnAccessCodeDialogClosed : BackupAction()
data object PrepareToWritePrimaryCard : BackupAction()
data object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
data class BackupFinished(val userWalletId: UserWalletId?) : BackupAction()
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data class ResumeFoundUnfinishedBackup(
val unfinishedBackupScanResponse: ScanResponse?,
) : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}

View file

@ -1,849 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.Artwork
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
object OnboardingWalletMiddleware {
val handler = onboardingWalletMiddleware
}
private const val HIDE_PROGRESS_DELAY = 400L
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is OnboardingWallet2Action -> handleWallet2Action(action)
else -> handleWalletAction(action)
}
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWalletAction(action: Action) {
if (action !is OnboardingWalletAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
val onboardingWalletState = store.state.onboardingWalletState
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, notNullCard ->
mainScope.launch {
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
when {
card == null -> {
// it's possible when found unfinished backup
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> {
store.dispatch(BackupAction.FinishBackup())
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
}
}
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
val cardArtwork = when (onboardingManager) {
null -> {
// onboardingManager is null when backup started from previously interrupted
val primaryCardId = backupService.primaryCardId
val cardPublicKey = backupService.primaryPublicKey
if (primaryCardId != null && cardPublicKey != null) {
// uses when no scanResponse and backup state restored
loadArtworkForCard(
cardId = primaryCardId,
cardPublicKey = cardPublicKey,
defaultArtwork = action.cardArtworkUriForUnfinishedBackup,
)
} else {
action.cardArtworkUriForUnfinishedBackup
}
}
else -> onboardingManager.loadArtworkUrl()
.takeIf { it != Artwork.DEFAULT_IMG_URL }
?.let { Uri.parse(it) }
}
store.dispatchOnMain(OnboardingWalletAction.SetPrimaryCardArtworkUrl(cardArtwork))
}
}
is OnboardingWalletAction.CreateWallet -> {
scanResponse ?: return
scope.launch {
val result = tangemSdkManager.createProductWallet(
scanResponse,
globalState.onboardingState.shouldResetOnCreate,
)
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result))
}
}
is OnboardingWalletAction.WalletWasCreated -> {
scanResponse ?: return
when (val result = action.result) {
is CompletionResult.Success -> {
if (action.shouldSendAnalyticsEvent) {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
}
// here we must use updated scanResponse after createWallet & derivation
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
mainScope.launch {
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatchWithMain(OnboardingWalletAction.ResumeBackup)
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
}
}
}
is OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
navigateToWalletScreen()
store.dispatch(BackupAction.DiscardBackup)
}
is OnboardingWalletAction.ResumeBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> {
if (onboardingWalletState.backupState.backupStep == null ||
onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
) {
Analytics.send(Onboarding.Backup.ScreenOpened())
val isWallet2 = scanResponse?.cardTypesResolver?.isWallet2() ?: false
if (isWallet2) BackupAction.StartBackup else BackupAction.IntroduceBackup
} else {
null
}
}
}
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> handleOnBackPressed(onboardingWalletState)
else -> Unit
}
}
private suspend fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build()
if (backupState.hasRing && userWalletId != null) {
scope.launch {
store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId)
}
}
OnboardingHelper.saveWallet(
alreadyCreatedWallet = userWallet,
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
hasBackupError = backupState.hasBackupError,
)
}
private fun navigateToWalletScreen() {
mainScope.launch {
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
}
}
}
private suspend fun readCard(): CompletionResult<ScanResponse> {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
return store.inject(DaggerGraphState::scanCardProcessor).scan(analyticsSource = ScreensSources.Intro)
}
private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri {
val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier)
return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action) {
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
when (action) {
is OnboardingWallet2Action.Init -> {
if (scanResponse?.cardTypesResolver?.isWallet2() == true) {
store.dispatch(OnboardingWallet2Action.SetDependencies(action.maxProgress))
}
}
is OnboardingWallet2Action.CreateWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.createProductWallet(
scanResponse,
globalState.onboardingState.shouldResetOnCreate,
)
) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.ImportWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
passphrase = action.passphrase,
mnemonic = action.mnemonicComponents.joinToString(" "),
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
) {
is CompletionResult.Success -> {
val creationType = when (action.seedPhraseSource) {
SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport
SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed
}
Analytics.send(
event = Onboarding.CreateWallet.WalletCreatedSuccessfully(
creationType = creationType,
seedPhraseLength = action.mnemonicComponents.size,
),
)
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.WalletWasCreated -> {
val result = when (val mediateResult = action.result) {
is CompletionResult.Success -> {
val response = CreateProductWalletTaskResponse(
card = mediateResult.data.card,
derivedKeys = mediateResult.data.derivedKeys,
primaryCard = mediateResult.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(mediateResult.error)
}
}
// do not send analytics event for wallet2 flow, cause its already sent
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(false, result))
}
else -> Unit
}
}
private fun handleActivationError() {
store.dispatchDialogShow(
OnboardingDialog.WalletActivationError(
onConfirm = {
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true))
},
),
)
}
private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse {
val card = if (backupState.backupCardsNumber > 0) {
val cardsCount = backupState.backupCardsNumber
scanResponse.card.copy(
backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount),
isAccessCodeSet = true,
)
} else {
scanResponse.card
}
return scanResponse.copy(card = card)
}
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
val globalState = appState()?.globalState ?: return
val onboardingWalletState = appState()?.onboardingWalletState ?: return
val backupState = onboardingWalletState.backupState
var scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
val card = scanResponse?.card
when (action) {
is BackupAction.StartBackup -> {
Analytics.send(Onboarding.Backup.Started())
backupService.discardSavedBackup()
val primaryCard = scanResponse?.primaryCard
if (primaryCard != null) {
backupService.setPrimaryCard(primaryCard)
store.dispatch(BackupAction.StartAddingBackupCards)
} else {
store.dispatch(BackupAction.StartAddingPrimaryCard)
}
}
is BackupAction.ScanPrimaryCard -> {
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = card?.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
}
is CompletionResult.Failure -> Unit
}
}
}
is BackupAction.AddBackupCard -> {
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(true))
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
store.inject(DaggerGraphState::cardSdkConfigRepository).sdk.config.filter.cardIdFilter = null
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false))
when (result) {
is CompletionResult.Success -> {
updateArtworks(backupService.addedBackupCardsCount, result.data)
store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data))
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.CardVerificationFailed -> {
Analytics.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
ScreensSources.Backup,
),
)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: com.tangem.core.ui.R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.CardAttestationFailed)
}
},
),
)
}
is TangemSdkError.BackupFailedNotEmptyWallets -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard(error.cardId),
),
)
}
is TangemSdkError.IssuerSignatureLoadingFailed -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(BackupDialog.AttestationFailed),
)
}
else -> Analytics.sendException(ExceptionAnalyticsEvent(error))
}
}
}
}
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == backupState.maxBackupCards) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
} else {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
}
is BackupAction.ShowEnterAccessCodeScreen -> {
Analytics.send(Onboarding.Backup.SettingAccessCodeStarted())
}
is BackupAction.CheckAccessCode -> {
Analytics.send(Onboarding.Backup.AccessCodeEntered())
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
} else {
store.dispatch(BackupAction.SetAccessCodeError(null))
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
Analytics.send(Onboarding.Backup.AccessCodeReEntered())
if (action.accessCodeConfirmation == backupState.accessCode) {
store.dispatch(BackupAction.SetAccessCodeError(null))
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
}
}
is BackupAction.WritePrimaryCard -> {
val isRing = scanResponse?.cardTypesResolver?.isRing() == true
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
}
is CompletionResult.Failure -> Unit
}
tangemSdkManager.clearProductType()
}
}
is BackupAction.WriteBackupCard -> {
val cardIndex = if (action.cardNumber > 0) action.cardNumber - 1 else action.cardNumber
val backupCard = backupState.backupCards.getOrNull(cardIndex)
val isRing = backupCard.isRing()
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) {
store.dispatchOnMain(BackupAction.ErrorInBackupCard)
}
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup())
} else {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
}
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.BackupFailedNotEmptyWallets -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard(error.cardId),
),
)
}
}
}
}
tangemSdkManager.clearProductType()
}
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.primaryCardId?.let { // always null for onboarding v2
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(it)
}
backupService.discardSavedBackup()
val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles)
.isOnboardingV2Enabled
if (isOnboardingV2Enabled) {
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Onboarding.Finished())
}
}
}
is BackupAction.CheckForUnfinishedBackup -> {
val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled
if (isOnboardingV2Enabled) {
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
mainScope.launch {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
}
} else if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound()))
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
// onboarding V2
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
} else {
store.dispatch(
GlobalAction.Onboarding.StartForUnfinishedBackup(
addedBackupCardsCount = backupService.addedBackupCardsCount,
),
)
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
}
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
Analytics.send(Onboarding.Finished())
scope.launch {
launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
with(scanResponse) {
if (this == null) {
when (val result = readCard()) {
is CompletionResult.Success -> handleFinishBackup(result.data)
is CompletionResult.Failure -> store.dispatchNavigationAction(AppRouter::pop)
}
} else {
handleFinishBackup(scanResponse = this)
}
}
}
}
is BackupAction.FinishBackup -> {
scope.launch {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
var userWallet: UserWallet? = null
if (scanResponse != null) {
scanResponse = updateScanResponseAfterBackup(scanResponse!!, backupState)
userWallet = createUserWallet(
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
backupState = backupState,
)
} else {
delay(HIDE_PROGRESS_DELAY)
when (val result = readCard()) {
is CompletionResult.Failure -> {
store.dispatchNavigationAction(AppRouter::pop)
return@launch
}
is CompletionResult.Success -> {
scanResponse = result.data
userWallet = createUserWallet(scanResponse = result.data, backupState = backupState)
}
}
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (backupState.startedSource) {
BackupStartedSource.Onboarding -> saveWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
scanResponse = scanResponse,
backupState = backupState,
)
BackupStartedSource.CreateBackup -> updateWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
backupState = backupState,
)
}
scope.launch {
if (userWallet.scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported) {
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(userWallet.walletId)
}
}
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
null
} else {
it
}
}
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) {
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWallet.walletId))
return@launch
}
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
handleFinishBackup(requireNotNull(scanResponse), userWallet)
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet.walletId))
}
}
is BackupAction.ResetBackupCard -> {
scope.launch { tangemSdkManager.resetToFactorySettings(action.cardId, false) }
}
else -> Unit
}
}
private fun Card?.isRing(): Boolean {
return this?.let { RING_BATCH_IDS.contains(batchId) || batchId.startsWith(RING_BATCH_PREFIX) } ?: false
}
private suspend fun saveWallet(
userWalletsListManager: UserWalletsListManager,
userWallet: UserWallet,
scanResponse: ScanResponse?,
backupState: BackupState,
) {
userWalletsListManager.save(
userWallet = userWallet.copy(
scanResponse = updateScanResponseAfterBackup(
scanResponse = requireNotNull(
value = scanResponse,
lazyMessage = { "ScanResponse is null" },
),
backupState = backupState,
),
),
canOverride = true,
)
}
private suspend fun updateWallet(
userWalletsListManager: UserWalletsListManager,
userWallet: UserWallet,
backupState: BackupState,
) {
userWalletsListManager.update(
userWalletId = userWallet.walletId,
update = { wallet ->
wallet.copy(
scanResponse = updateScanResponseAfterBackup(
scanResponse = wallet.scanResponse,
backupState = backupState,
),
)
},
)
}
private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet {
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
return requireNotNull(
value = userWalletBuilder
.backupCardsIds(backupState.backupCardIds.toSet())
.hasBackupError(backupState.hasBackupError)
.build(),
lazyMessage = { "User wallet not created" },
)
}
fun updateArtworks(addedBackupCardsCount: Int, card: Card) {
mainScope.launch {
withIOContext {
val imageUri = loadArtworkForCard(card.cardId, card.cardPublicKey, Uri.EMPTY)
when (addedBackupCardsCount) {
1 -> {
store.dispatchOnMain(OnboardingWalletAction.SetSecondCardArtworkUrl(imageUri))
}
2 -> {
store.dispatchOnMain(OnboardingWalletAction.SetThirdCardArtworkUrl(imageUri))
}
}
}
}
}
internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> {
return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.filterNotNull()
.distinct()
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
null, BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
-> {
showInterruptOnboardingDialog()
}
is BackupStep.WriteBackupCard -> {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
BackupStep.Finished -> {
OnboardingHelper.onInterrupted()
store.dispatchNavigationAction(AppRouter::pop)
}
}
}
private fun showInterruptOnboardingDialog() {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(BackupAction.DiscardBackup)
store.dispatchNavigationAction(AppRouter::pop)
},
),
)
}

View file

@ -1,185 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.redux.OnboardingManageTokensAction
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingWalletReducer {
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
}
@Suppress("CyclomaticComplexMethod")
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
val state = appState.onboardingWalletState
val backupState = state.backupState
return when (action) {
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
is BackupAction.BackupFinished -> {
state.copy(
step = if (action.userWalletId != null && backupState.startedSource == BackupStartedSource.Onboarding) {
OnboardingWalletStep.ManageTokens
} else {
OnboardingWalletStep.Done
},
userWalletId = action.userWalletId,
)
}
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action = action, state = state.backupState))
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(primaryCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetSecondCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(secondCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetThirdCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri),
)
}
is OnboardingManageTokensAction.CurrenciesSaved -> state.copy(step = OnboardingWalletStep.Done)
is OnboardingWalletAction.WalletSaved -> state.copy(
step = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> OnboardingWalletStep.ManageTokens
BackupStartedSource.CreateBackup -> OnboardingWalletStep.Done
},
userWalletId = action.userWalletId,
)
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
else -> state
}
}
private object ReducerForGlobalAction {
private const val MAX_BACKUP_CARDS = 2
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState {
return when (action) {
is GlobalAction.Onboarding.Start -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = MAX_BACKUP_CARDS,
canSkipBackup = action.canSkipBackup,
startedSource = action.source,
),
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = action.addedBackupCardsCount,
canSkipBackup = false,
isInterruptedBackup = true,
),
)
}
else -> state
}
}
}
private object OnboardingWallet2Reducer {
fun reduce(action: OnboardingWallet2Action, state: OnboardingWalletState): OnboardingWalletState = when (action) {
is OnboardingWallet2Action.SetDependencies -> state.copy(
wallet2State = OnboardingWallet2State(
maxProgress = action.maxProgress,
),
)
else -> state
}
}
private object BackupReducer {
@Suppress("ComplexMethod", "LongMethod")
fun reduce(action: BackupAction, state: BackupState): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
startedSource = state.startedSource,
)
BackupAction.StartAddingPrimaryCard -> {
state.copy(
backupStep = BackupStep.ScanOriginCard,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
)
}
BackupAction.StartAddingBackupCards -> {
state.copy(
backupStep = BackupStep.AddBackupCards,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
)
}
is BackupAction.AddBackupCard.Success -> {
state.copy(
backupCards = state.backupCards + action.card,
backupCardsNumber = state.backupCardsNumber + 1,
)
}
is BackupAction.AddBackupCard.ChangeButtonLoading -> state.copy(showBtnLoading = action.isLoading)
BackupAction.ShowAccessCodeInfoScreen -> state.copy(backupStep = BackupStep.SetAccessCode)
BackupAction.ShowEnterAccessCodeScreen -> state.copy(
backupStep = BackupStep.EnterAccessCode,
accessCodeError = null,
)
is BackupAction.SaveFirstAccessCode -> state.copy(
backupStep = BackupStep.ReenterAccessCode,
accessCode = action.accessCode,
)
is BackupAction.SetAccessCodeError -> state.copy(accessCodeError = action.error)
is BackupAction.PrepareToWritePrimaryCard -> if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
primaryCardBatchId = backupService.primaryCardBatchId,
backupCardIds = backupService.backupCardIds,
backupCardBatchIds = backupService.backupCardsBatchIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WritePrimaryCard,
)
} else {
state.copy(backupStep = BackupStep.WritePrimaryCard)
}
is BackupAction.PrepareToWriteBackupCard -> if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
primaryCardBatchId = backupService.primaryCardBatchId,
backupCardIds = backupService.backupCardIds,
backupCardBatchIds = backupService.backupCardsBatchIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WriteBackupCard(action.cardNumber),
)
} else {
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true)
is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished)
is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished)
BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards)
BackupAction.DiscardBackup -> BackupState()
is BackupAction.SetHasRing -> {
// Don't update if state.has ring is already true
if (state.hasRing) {
state
} else {
state.copy(hasRing = action.hasRing)
}
}
else -> state
}
}
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.common.card.Card
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.UserWalletId
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val walletImages: WalletImages = WalletImages(),
val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false,
val userWalletId: UserWalletId? = null,
) : StateType {
@Suppress("MagicNumber")
fun getMaxProgress(): Int {
val baseProgress = 7
return getWallet2Progress() + baseProgress
}
@Suppress("ComplexMethod", "MagicNumber")
fun getProgressStep(): Int {
val progressByStep = when (step) {
OnboardingWalletStep.CreateWallet, OnboardingWalletStep.None -> 1
OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
null -> 2
BackupStep.InitBackup -> 2
BackupStep.ScanOriginCard -> 3
BackupStep.AddBackupCards -> 4
BackupStep.EnterAccessCode -> 4
BackupStep.ReenterAccessCode -> 4
BackupStep.SetAccessCode -> 4
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
BackupStep.Finished -> 6
}
}
OnboardingWalletStep.ManageTokens -> 6
OnboardingWalletStep.Done -> getMaxProgress()
}
return getWallet2Progress() + progressByStep
}
private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0
}
data class WalletImages(
val primaryCardImage: Uri? = null,
val secondCardImage: Uri? = null,
val thirdCardImage: Uri? = null,
)
data class OnboardingWallet2State(
val maxProgress: Int,
)
enum class OnboardingWalletStep {
None, CreateWallet, Backup, ManageTokens, Done
}
data class BackupState(
val primaryCardId: String? = null,
val primaryCardBatchId: String? = null,
val backupCardsNumber: Int = 0,
val backupCards: List<Card> = emptyList(),
val backupCardIds: List<CardId> = emptyList(),
val backupCardBatchIds: List<CardId> = emptyList(),
@Transient
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep? = null,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
val startedSource: BackupStartedSource = BackupStartedSource.Onboarding,
val hasRing: Boolean = false,
)
enum class BackupStartedSource {
Onboarding, CreateBackup
}
enum class AccessCodeError {
CodeTooShort, CodesDoNotMatch
}
typealias CardId = String
sealed class BackupStep {
data object InitBackup : BackupStep()
data object ScanOriginCard : BackupStep()
data object AddBackupCards : BackupStep()
data object SetAccessCode : BackupStep()
data object EnterAccessCode : BackupStep()
data object ReenterAccessCode : BackupStep()
data object WritePrimaryCard : BackupStep()
data class WriteBackupCard(val cardNumber: Int) : BackupStep()
data object Finished : BackupStep()
}
sealed class BackupDialog : StateDialog {
data object AttestationFailed : BackupDialog()
data object AddMoreBackupCards : BackupDialog()
data object BackupInProgress : BackupDialog()
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ResetBackupCard(val cardId: String) : BackupDialog()
}

View file

@ -1,314 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.*
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
/**
[REDACTED_AUTHOR]
*/
interface BackupAnimator {
fun updateBackupState(backupState: BackupState)
fun setupCreateWalletState()
fun showBackupIntro(state: BackupState)
fun showScanOriginCard()
fun showAddBackupCards(state: BackupState, backupCardsCounts: Int)
fun showWritePrimaryCard(state: BackupState)
fun showWriteBackupCard(state: BackupState, backupCard: Int)
fun showSuccess(onEnd: () -> Unit)
}
class WalletBackupAnimator(
private val cardsWidget: WalletCardsWidget,
) : BackupAnimator {
private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) {
2 -> State.TwoCards
3 -> State.ThreeCards
else -> throw UnsupportedOperationException()
}
private var currentStep = -1
private var firstBackupCardAnimated = false
private var secondBackupCardAnimated = false
override fun updateBackupState(backupState: BackupState) {
}
override fun showBackupIntro(state: BackupState) {
currentStep = STEP_BACKUP_INTRO
ORIGIN.show()
FIRST_BACKUP.show()
SECOND_BACKUP.show()
cardsWidget.toWelcome()
}
override fun setupCreateWalletState() {
currentStep = STEP_CREATE_WALLET
cardsWidget.toFolded(false)
ORIGIN.alpha(1f)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(1f)
}
override fun showScanOriginCard() {
currentStep = STEP_SCAN_ORIGIN_CARD
cardsWidget.toFolded()
}
@Suppress("MagicNumber")
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
when (backupCardsCounts) {
0 -> {
when (currentStep) {
UNDEFINED, STEP_CREATE_WALLET -> {
cardsWidget.toFolded(false) { cardsWidget.toFan() }
FIRST_BACKUP.animateAlpha(0.6f, 0)
SECOND_BACKUP.animateAlpha(0.2f, 400)
}
STEP_BACKUP_INTRO -> {
cardsWidget.toFolded(duration = 300) { cardsWidget.toFan() }
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
}
STEP_SCAN_ORIGIN_CARD -> {
cardsWidget.toFan()
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
}
}
}
1 -> {
cardsWidget.toFan(false)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(0.2f)
}
2 -> {
cardsWidget.toFan(false)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(1f)
}
}
currentStep = STEP_ADD_BACKUP_CARDS
}
@Suppress("MagicNumber")
override fun showWritePrimaryCard(state: BackupState) {
currentStep = STEP_WRITE_PRIMARY_CARD
cardsWidget.toLeapfrog(
onEndFold = {
FIRST_BACKUP.alpha(0.4f)
SECOND_BACKUP.alpha(0.2f)
},
)
}
@Suppress("MagicNumber")
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
when (viewState) {
State.TwoCards -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
val delay = if (currentStep == UNDEFINED) 700L else 0
postUiDelayBg(delay) {
FIRST_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
ORIGIN.alpha(0.4f)
}
}
}
State.ThreeCards -> {
when (backupCard) {
1 -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
FIRST_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
SECOND_BACKUP.alpha(0.4f)
ORIGIN.alpha(0.2f)
}
}
2 -> {
if (secondBackupCardAnimated) return
secondBackupCardAnimated = true
/*
* !!! Workaround !!!
* For interrupted backup
* */
if (!firstBackupCardAnimated) {
showWriteBackupCard(state, backupCard = 1)
return
}
SECOND_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
ORIGIN.alpha(0.4f)
FIRST_BACKUP.alpha(0.2f)
}
}
}
}
}
currentStep = STEP_WRITE_BACKUP_CARD
}
override fun showSuccess(onEnd: () -> Unit) {
currentStep = STEP_SUCCESS
cardsWidget.leapfrogWidget.fold(onEnd = onEnd)
}
private enum class State {
ThreeCards,
TwoCards,
}
private fun BackupCardType.animateAlpha(alpha: Float, duration: Long = 0L, startDelay: Long = 0L) {
when (this) {
ORIGIN -> {
cardsWidget.getOriginCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
FIRST_BACKUP -> {
cardsWidget.getFirstBackupCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
}
}
private fun BackupCardType.alpha(alpha: Float) {
when (this) {
ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().alpha = alpha
}
}
}
private fun BackupCardType.show() {
when (this) {
ORIGIN -> cardsWidget.getOriginCardView().show()
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show()
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().show()
}
}
}
companion object {
private const val UNDEFINED = -1
private const val STEP_CREATE_WALLET = 0
private const val STEP_BACKUP_INTRO = 1
private const val STEP_SCAN_ORIGIN_CARD = 2
private const val STEP_ADD_BACKUP_CARDS = 3
private const val STEP_WRITE_PRIMARY_CARD = 4
private const val STEP_WRITE_BACKUP_CARD = 5
private const val STEP_SUCCESS = 6
}
}
class TestBackupAnimation(
private val animator: BackupAnimator,
binding: FragmentOnboardingWalletBinding,
) : BackupAnimator {
private lateinit var state: BackupState
private var steps = 0
init {
init(binding, this)
}
@Suppress("MagicNumber")
private fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) {
steps = step
when (steps) {
0 -> setupCreateWalletState()
1 -> showBackupIntro(state)
2 -> showScanOriginCard()
3 -> showAddBackupCards(state, 0)
4 -> showAddBackupCards(state, 1)
5 -> showAddBackupCards(state, 2)
6 -> showWritePrimaryCard(state)
7 -> showWriteBackupCard(state, 1)
8 -> showWriteBackupCard(state, 2)
else -> setStep(0)
}
onStepUpdate(steps)
}
fun onNext(onStepUpdate: (Int) -> Unit) {
steps++
setStep(steps, onStepUpdate)
}
override fun updateBackupState(backupState: BackupState) {
this.state = backupState
}
override fun setupCreateWalletState() {
animator.setupCreateWalletState()
}
override fun showBackupIntro(state: BackupState) {
animator.showBackupIntro(state)
}
override fun showScanOriginCard() {
animator.showScanOriginCard()
}
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
animator.showAddBackupCards(state, backupCardsCounts)
}
override fun showWritePrimaryCard(state: BackupState) {
animator.showWritePrimaryCard(state)
}
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
animator.showWriteBackupCard(state, backupCard)
}
override fun showSuccess(onEnd: () -> Unit) {
animator.showSuccess(onEnd)
}
companion object {
fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) {
val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" }
button.setOnClickListener { animator.onNext { button.text = "$it" } }
binding.onboardingRoot.addView(button, 0)
}
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getString
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemBackupInfoAdapterBinding
class BackupInfoAdapter : RecyclerView.Adapter<BackupInfoViewHolder>() {
private val data = backupInfoSnippets
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder = BackupInfoViewHolder(
ItemBackupInfoAdapterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
),
)
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) = holder.binding.run {
tvHeader.text = root.getString(data[position].header)
tvBody.text = root.getString(data[position].body)
}
}
private class BackupInfo(
@StringRes val header: Int,
@StringRes val body: Int,
)
private val backupInfoSnippets = listOf(
BackupInfo(
R.string.onboarding_wallet_info_title_first,
R.string.onboarding_wallet_info_subtitle_first,
),
BackupInfo(
R.string.onboarding_wallet_info_title_second,
R.string.onboarding_wallet_info_subtitle_second,
),
BackupInfo(
R.string.onboarding_wallet_info_title_third,
R.string.onboarding_wallet_info_subtitle_third,
),
BackupInfo(
R.string.onboarding_wallet_info_title_fourth,
R.string.onboarding_wallet_info_subtitle_fourth,
),
)
class BackupInfoViewHolder(val binding: ItemBackupInfoAdapterBinding) :
RecyclerView.ViewHolder(binding.root)

View file

@ -1,82 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(),
private val activity: Activity,
) {
fun newState(
walletFragment: OnboardingWalletFragment,
state: OnboardingWalletState,
seedPhraseViewModel: SeedPhraseViewModel,
) {
if (state.step == OnboardingWalletStep.CreateWallet && !seedPhraseViewModel.isFinished) {
switchToWallet2SeedPhraseOnboarding(walletFragment, seedPhraseViewModel, state.getMaxProgress())
} else {
switchToWalletOnboarding(walletFragment, state)
}
}
private fun switchToWalletOnboarding(walletFragment: OnboardingWalletFragment, state: OnboardingWalletState) {
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
walletFragment.pbBinding.pbState.show()
walletFragment.binding.onboardingWalletContainer.show()
walletFragment.handleOnboardingStep(state)
}
private fun switchToWallet2SeedPhraseOnboarding(
walletFragment: OnboardingWalletFragment,
viewModel: SeedPhraseViewModel,
onboardingWalletMaxProgress: Int,
) {
walletFragment.pbBinding.pbState.hide()
walletFragment.binding.onboardingWalletContainer.hide()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.show()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.setContent {
val subScreen = viewModel.currentScreen.collectAsState().value
setMainScreenToolbarTitle(walletFragment, subScreen)
TangemTheme(
isDark = isSystemInDarkTheme(),
windowSize = rememberWindowSize(activity = activity),
) {
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = subScreen,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
}
}
}
private fun setMainScreenToolbarTitle(walletFragment: OnboardingWalletFragment, subScreen: SeedPhraseScreen) {
val titleResId = when (subScreen) {
SeedPhraseScreen.Intro -> R.string.wallet_title
SeedPhraseScreen.AboutSeedPhrase,
SeedPhraseScreen.YourSeedPhrase,
SeedPhraseScreen.CheckSeedPhrase,
-> R.string.onboarding_create_wallet_header
SeedPhraseScreen.ImportSeedPhrase -> R.string.onboarding_seed_intro_button_import
}
walletFragment.binding.toolbar.title = walletFragment.getString(titleResId)
}
}

View file

@ -1,691 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.arkivanov.decompose.defaultComponentContext
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
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.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.datasource.utils.isNullOrEmpty
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.isRing
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import timber.log.Timber
import javax.inject.Inject
@Suppress("LargeClass", "MagicNumber")
@AndroidEntryPoint
class OnboardingWalletFragment :
BaseFragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>,
FragmentOnBackPressedHandler {
@Inject
internal lateinit var uiDependencies: UiDependencies
@Inject
internal lateinit var onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory
@Inject
@RootAppComponentContext
internal lateinit var rootComponentContext: AppComponentContext
private var onboardingManageTokensComponent: OnboardingManageTokensComponent? = null
private lateinit var onboardingComponentContext: AppComponentContext
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
private val bindingManageTokens by lazy { binding.onboardingManageTokensContainer }
private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true }
private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private lateinit var cardsWidget: WalletCardsWidget
private var seedPhraseRouter: SeedPhraseRouter? = null
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var animator: BackupAnimator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity())
val newSeedPhraseRouter = makeSeedPhraseRouter()
seedPhraseRouter = newSeedPhraseRouter
seedPhraseViewModel.setRouter(newSeedPhraseRouter)
seedPhraseViewModel.setMediator(makeSeedPhraseMediator())
onboardingComponentContext = rootComponentContext.childByContext(
componentContext = defaultComponentContext(onBackPressedDispatcher = null),
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val scaleFactor = getDeviceScaleFactor()
initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor)
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
TabLayoutMediator(
binding.tabLayoutBackupInfo,
binding.viewPagerBackupInfo,
) { tab, position ->
// Some implementation
}.attach()
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
addBackPressHandler(handler = this)
store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress))
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(
OnboardingWalletAction.LoadArtwork(
cardArtworkUriForUnfinishedBackup = requireContext().resourceUri(R.drawable.card_placeholder_wallet),
),
)
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null")
},
)
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1
if (viewBackupCount <= 0) return@with
if (viewBackupCount == backupCardsCounts) return@with
cardsWidget.toFolded(false)
if (viewBackupCount > backupCardsCounts) {
flCardsContainer.removeViews(0, viewBackupCount - backupCardsCounts)
} else {
flCardsContainer.inflate(R.layout.view_onboarding_card, true)
}
val scaleFactor = getDeviceScaleFactor()
initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor)
}
private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget {
val leapfrogCalculator = PropertyCalculator(
yTranslationFactor = when (container.childCount) {
3 -> 25f
else -> 35f
} * deviceScaleFactor,
)
return LeapfrogWidget(container, leapfrogCalculator)
}
private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) {
cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor)
animator = if (isTest) {
TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding)
} else {
WalletBackupAnimator(cardsWidget)
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingWalletState == newState.onboardingWalletState
}.select { it.onboardingWalletState }
}
setStatusBarColor(R.color.background_primary)
}
override fun onStop() {
super.onStop()
onboardingManageTokensComponent = null
store.unsubscribe(this)
}
override fun newState(state: OnboardingWalletState) {
Handler(Looper.getMainLooper()).post {
if (activity == null || view == null) return@post
animator.updateBackupState(state.backupState)
requireActivity().invalidateOptionsMenu()
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
when {
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
seedPhraseViewModel.setCardArtworkUri(
cardArtworkUri = state.walletImages.primaryCardImage.toString(),
)
updateWalletImagesState(state.walletImages)
}
else -> {
updateWalletImagesState(state.walletImages)
handleOnboardingStep(state)
}
}
}
}
private fun updateWalletImagesState(walletImages: WalletImages) {
if (!walletImages.primaryCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.primaryCardImage, binding.imvFrontCard)
}
if (!walletImages.secondCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.secondCardImage, binding.imvFirstBackupCard)
}
if (!walletImages.thirdCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.thirdCardImage, binding.imvSecondBackupCard)
}
}
private fun loadImageIntoImageView(uri: Uri?, view: ImageView) {
view.load(uri) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
internal fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state = state.backupState)
OnboardingWalletStep.ManageTokens -> setManageTokensState(requireNotNull(state.userWalletId))
OnboardingWalletStep.Done -> showSuccess()
else -> {}
}
}
private fun initializeOnboardingManageTokensComponent(userWalletId: UserWalletId) {
onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create(
context = onboardingComponentContext,
params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId),
onDone = {},
)
}
private fun setManageTokensState(userWalletId: UserWalletId) {
with(binding) {
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
onboardingWalletContainer.show()
layoutButtonsCommon.btnWalletAlternativeAction.hide()
onboardingWalletContainer.hide()
toolbar.title = getText(R.string.onboarding_add_tokens)
}
pbBinding.pbState.show()
bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
if (onboardingManageTokensComponent == null) {
initializeOnboardingManageTokensComponent(userWalletId)
}
with(bindingManageTokens) {
onboardingManageTokensContainer.hide()
onboardingManageTokensContainer.show()
onboardingManageTokensContainer.setContent {
TangemTheme(
isDark = isSystemInDarkTheme(),
windowSize = rememberWindowSize(activity = requireActivity()),
) {
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
}
}
}
}
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingWalletAction.CreateWallet)
}
layoutButtonsCommon.btnWalletAlternativeAction.hide()
toolbar.title = getText(R.string.onboarding_getting_started)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
animator.setupCreateWalletState()
}
private fun setBackupState(state: BackupState) {
when (state.backupStep) {
null -> Unit
BackupStep.InitBackup -> showBackupIntro(state)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
BackupStep.Finished -> {
// don't need to navigate here"
// showSuccess()
}
}
}
private fun showBackupIntro(state: BackupState) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
tvHeader.hide()
tvBody.hide()
viewPagerBackupInfo.show()
tabLayoutBackupInfo.show()
with(layoutButtonsCommon) {
btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now)
btnWalletMainAction.icon = null
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && canSkipBackup)
}
animator.showBackupIntro(state)
}
private fun showScanOriginCard() = with(binding) {
prepareBackupView()
tvHeader.text = getText(R.string.onboarding_title_scan_origin_card)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary,
)
with(layoutButtonsCommon) {
btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
btnWalletAlternativeAction.hide()
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
}
animator.showScanOriginCard()
}
private fun showAddBackupCards(state: BackupState) = with(binding) {
prepareBackupView()
accessCodeDialog?.dismiss()
accessCodeDialog = null
layoutButtonsAddCards.root.show()
layoutButtonsCommon.root.hide()
layoutButtonsAddCards.btnAddCard.setIconResource(R.drawable.ic_tangem_24)
if (state.showBtnLoading) {
layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf(
resources.getColor(R.color.button_secondary),
)
layoutButtonsAddCards.btnAddCard.text = ""
layoutButtonsAddCards.btnAddCard.isEnabled = false
layoutButtonsAddCards.btnProgress.show()
} else {
layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf(
resources.getColor(R.color.icon_primary_1),
)
layoutButtonsAddCards.btnProgress.hide()
layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card)
layoutButtonsAddCards.btnAddCard.isEnabled = true
}
if (state.backupCardsNumber < state.maxBackupCards) {
layoutButtonsAddCards.btnAddCard.setOnClickListener {
store.dispatch(BackupAction.AddBackupCard)
}
} else {
layoutButtonsAddCards.btnAddCard.isEnabled = false
}
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
when (state.backupCardsNumber) {
0 -> {
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
}
2 -> {
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
}
else -> {}
}
animator.showAddBackupCards(state, state.backupCardsNumber)
}
private fun prepareBackupView() = with(binding) {
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
}
private fun showSetAccessCode() {
accessCodeDialog = AccessCodeDialog(requireContext()).apply {
dismissWithAnimation = true
create()
setOnCancelListener {
store.dispatch(BackupAction.OnAccessCodeDialogClosed)
}
setOnShowListener { setSecurityMode(true) }
setOnDismissListener { setSecurityMode(false) }
show()
showInfoScreen()
val view =
delegate.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
(view?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) }
behavior.state = BottomSheetBehavior.STATE_EXPANDED
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
view?.let { it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT }
}
}
private fun showEnterAccessCode(state: BackupState) {
accessCodeDialog?.showEnterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showReenterAccessCode(state: BackupState) {
accessCodeDialog?.showReenterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showWritePrimaryCard(state: BackupState) = with(binding) {
accessCodeDialog?.dismiss()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
val isRing = state.primaryCardBatchId != null && isRing(batchId = state.primaryCardBatchId)
tvHeader.text = getText(
if (isRing) R.string.common_origin_ring else R.string.common_origin_card,
)
tvBody.text = if (isRing) {
getString(R.string.onboarding_subtitle_scan_ring)
} else {
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
}
layoutButtonsCommon.btnWalletMainAction.text = getText(
if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin,
)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
animator.showWritePrimaryCard(state)
}
private fun prepareViewForFinalizeStep() = with(binding) {
toolbar.title = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.root.hide()
layoutButtonsCommon.root.show()
layoutButtonsCommon.btnWalletAlternativeAction.hide()
imvCardBackground.hide()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
}
private fun showWriteBackupCard(state: BackupState) = with(binding) {
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
val isRing = isRing(batchId = state.backupCardBatchIds[cardNumber - 1])
tvHeader.text = getString(
if (isRing) R.string.onboarding_title_backup_ring else R.string.onboarding_title_backup_card,
)
tvBody.text = if (isRing) {
getString(R.string.onboarding_subtitle_scan_ring)
} else {
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
)
}
layoutButtonsCommon.btnWalletMainAction.text = getString(
if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin,
)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
}
animator.showWriteBackupCard(state, cardNumber)
}
private fun showSuccess() = with(binding) {
toolbar.title = getString(R.string.onboarding_done_header)
tvHeader.text = getText(R.string.onboarding_done_header)
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
onboardingWalletContainer.show()
bindingManageTokens.onboardingManageTokensContainer.hide()
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
layoutButtonsCommon.btnWalletMainAction.icon = null
layoutButtonsCommon.btnWalletAlternativeAction.hide()
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope))
}
animator.showSuccess {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
flCardsContainer.hide()
imvCardBackground.hide()
showConfetti(true)
imvSuccess.alpha = 0f
imvSuccess.show()
imvSuccess.animate()?.alpha(1f)?.duration = 400
}
}
}
internal fun showConfetti(show: Boolean) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lavConfetti.playAnimation()
} else {
lavConfetti.cancelAnimation()
}
}
}
}
override fun handleOnBackPressed() {
// workaround to use right navigation back for toolbar back btn on seed phrase flow
val isWallet2 =
store.state.globalState.onboardingState.onboardingManager?.scanResponse?.cardTypesResolver?.isWallet2()
?: false
val seedPhraseRouter = seedPhraseRouter
if (seedPhraseRouter != null && isWallet2) {
seedPhraseRouter.navigateBack()
} else {
legacyOnBackHandler()
}
}
private fun legacyOnBackHandler() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
private fun getDeviceScaleFactor(): Float {
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
return typedValue.float
}
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::legacyOnBackHandler,
onOpenChat = {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val scanResponse = requireNotNull(store.state.globalState.onboardingState.onboardingManager?.scanResponse)
val getCardInfoUseCase = store.inject(DaggerGraphState::getCardInfoUseCase)
val cardInfo = requireNotNull(getCardInfoUseCase.invoke(scanResponse).getOrNull())
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
},
onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString())
},
)
private fun makeSeedPhraseMediator(): SeedPhraseMediator {
return object : SeedPhraseMediator {
override fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit) {
store.dispatch(OnboardingWallet2Action.CreateWallet(callback))
}
override fun onWalletCreated(result: CompletionResult<CreateWalletResponse>) {
store.dispatch(OnboardingWallet2Action.WalletWasCreated(result))
}
override fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) {
store.dispatch(
OnboardingWallet2Action.ImportWallet(
mnemonicComponents,
passphrase,
seedPhraseSource,
callback,
),
)
}
override fun allowScreenshots(allow: Boolean) {
setSecurityMode(value = !allow)
}
}
}
private fun setSecurityMode(value: Boolean) {
Timber.d("Security mode: ${if (value) "enabled" else "disabled"}")
if (value) {
requireActivity().window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
} else {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}

View file

@ -1,240 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import android.widget.ImageView
import androidx.core.animation.doOnEnd
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
) {
private val animDuration: Long = 400
private var currentState: WidgetState? = null
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.WELCOME) return
currentState = WidgetState.WELCOME
val animator = createAnimator(animate, onEnd = onEnd)
animator.playTogether(createAnimators(::createWelcomeProperties))
leapfrogWidget.fold { animator.start() }
}
fun toFolded(animate: Boolean = true, duration: Long = animDuration, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.FOLDED) return
currentState = WidgetState.FOLDED
val animator = createAnimator(animate, duration) {
leapfrogWidget.fold(animate, onEnd)
}
animator.playTogether(createAnimators(::createLeapfrogProperties))
animator.start()
}
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.FAN) return
currentState = WidgetState.FAN
val animator = createAnimator(animate, onEnd = onEnd)
animator.playTogether(createAnimators(::createFanProperties))
leapfrogWidget.fold { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEndFold: () -> Unit = {}, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.LEAPFROG) return
currentState = WidgetState.LEAPFROG
val onAnimatorEnd = {
leapfrogWidget.fold(animate) {
leapfrogWidget.initViews()
onEndFold()
leapfrogWidget.unfold(animate, onEnd)
}
}
val animator = createAnimator(animate, onEnd = onAnimatorEnd)
animator.playTogether(createAnimators(::createLeapfrogProperties))
animator.start()
}
private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List<ObjectAnimator> {
return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) }
}
@Suppress("MagicNumber")
private fun getCardTypesTypes(): List<BackupCardType> = when (leapfrogWidget.getViewsCount()) {
2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP)
3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP)
else -> throw UnsupportedOperationException()
}
private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet {
return AnimatorSet().apply {
this.duration = if (animate) duration else 0
doOnEnd { onEnd() }
}
}
private fun createCardAnimator(cardType: BackupCardType, properties: CardProperties): ObjectAnimator {
val view = getLeapViewByCard(cardType).view
val animator = ObjectAnimator.ofPropertyValuesHolder(
view,
*properties.createValuesHolders().toTypedArray(),
)
view.elevation = properties.elevation
return animator
}
@Suppress("MagicNumber")
private fun createWelcomeProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 440f * deviceScaleFactor,
yTranslation = 70f,
rotation = 75f,
elevation = 2f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 10f * deviceScaleFactor,
yTranslation = -30f,
rotation = 100f,
elevation = 1f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = -440f * deviceScaleFactor,
yTranslation = -30f,
rotation = 70f,
elevation = 0f,
scale = .85f * deviceScaleFactor,
)
}
}
@Suppress("MagicNumber")
private fun createFanProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 30f,
rotation = 5f,
elevation = 2f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = -50f,
rotation = -5f,
elevation = 1f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -120f,
rotation = -20f,
elevation = 0f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@Suppress("MagicNumber")
private fun createLeapfrogProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 2f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 1f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 0f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@Throws(UnsupportedOperationException::class)
fun getOriginCardView(): ImageView {
return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView
}
@Throws(UnsupportedOperationException::class)
fun getFirstBackupCardView(): ImageView {
return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView
}
@Throws(UnsupportedOperationException::class)
fun getSecondBackupCardView(): ImageView {
return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView
}
@Suppress("MagicNumber")
@Throws(UnsupportedOperationException::class)
private fun getLeapViewByCard(cardType: BackupCardType): LeapView {
fun getByIndex(index: Int): LeapView {
return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index))
}
return when (leapfrogWidget.getViewsCount()) {
2 -> when (cardType) {
BackupCardType.ORIGIN -> getByIndex(1)
BackupCardType.FIRST_BACKUP -> getByIndex(0)
BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException()
}
3 -> when (cardType) {
BackupCardType.ORIGIN -> getByIndex(2)
BackupCardType.FIRST_BACKUP -> getByIndex(1)
BackupCardType.SECOND_BACKUP -> getByIndex(0)
}
else -> throw UnsupportedOperationException()
}
}
enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG }
}
enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP }
private data class CardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,
val rotation: Float = 0f,
val elevation: Float = 0f,
val scale: Float = 1.1f,
) {
fun createValuesHolders(): List<PropertyValuesHolder> {
return listOf(
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
)
}
companion object
}

View file

@ -1,81 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding
class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
private var binding: LayoutBackupAccessCodeBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutBackupAccessCodeBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
super.setOnDismissListener {
listener?.onDismiss(it)
binding = null
}
}
fun showInfoScreen() = binding?.run {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
layoutBackupAccessCodeInfo.root.show()
layoutBackupAccessCodeSubmit.root.hide()
layoutBackupAccessCodeInfo.btnAccessCodeCreate.setOnClickListener {
store.dispatch(BackupAction.ShowEnterAccessCodeScreen)
}
}
fun showEnterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
with(layoutBackupAccessCodeSubmit) {
btnAccessCodeSubmit.text = context.getText(R.string.common_continue)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.CheckAccessCode(etAccessCode.text.toString()))
}
}
}
fun showReenterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
with(layoutBackupAccessCodeSubmit) {
etAccessCode.setText("")
btnAccessCodeSubmit.text = context.getText(R.string.common_submit)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.SaveAccessCodeConfirmation(etAccessCode.text.toString()))
}
}
}
fun showError(error: AccessCodeError?) = binding?.layoutBackupAccessCodeSubmit?.run {
when (error) {
AccessCodeError.CodeTooShort ->
tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short)
AccessCodeError.CodesDoNotMatch ->
tilAccessCode.error = context.getString(R.string.onboarding_access_codes_doesnt_match)
null -> tilAccessCode.error = null
}
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object AddMoreBackupCardsDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added)
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ShowAccessCodeInfoScreen)
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object AttestationFailedDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_error)
setMessage(R.string.issuer_signature_loading_failed)
setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object BackupInProgressDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_backup_exit_warning)
setPositiveButton(R.string.warning_button_ok) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object InterruptOnboardingDialog {
fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.onboarding_exit_alert_title))
setMessage(context.getString(R.string.onboarding_exit_alert_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object ResetBackupCardDialog {
fun create(context: Context, cardId: String): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_attention)
setMessage(R.string.onboarding_linking_error_card_with_wallets)
setPositiveButton(R.string.common_cancel) { _, _ ->
Analytics.send(Onboarding.Backup.ResetCancelEvent)
}
setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
Analytics.send(Onboarding.Backup.ResetPerformEvent)
store.dispatch(BackupAction.ResetBackupCard(cardId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.core.TangemError
import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.Action
internal sealed interface SaveWalletAction : Action {
data class ProvideBackupInfo(
val scanResponse: ScanResponse,
val accessCode: String?,
val backupCardsIds: Set<String>?,
) : SaveWalletAction
data object AllowToUseBiometrics : SaveWalletAction {
data object Success : SaveWalletAction
data class Error(val error: TangemError) : SaveWalletAction
}
data object Dismiss : SaveWalletAction
data object CloseError : SaveWalletAction
data object EnrollBiometrics : SaveWalletAction {
data object Enroll : SaveWalletAction
data object Cancel : SaveWalletAction
}
data class SaveWalletAfterBackup(
val hasBackupError: Boolean,
val shouldNavigateToWallet: Boolean,
) : SaveWalletAction
}

View file

@ -1,188 +0,0 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
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.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
internal class SaveWalletMiddleware {
private val saveWalletJobHolder = JobHolder()
val middleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
val state = stateProvider()
if (action is SaveWalletAction && state != null) {
handleAction(action, state.saveWalletState)
}
next(action)
}
}
}
private fun handleAction(action: SaveWalletAction, state: SaveWalletState) {
when (action) {
is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state)
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.Dismiss -> dismiss(state)
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(
state = state,
hasBackupError = action.hasBackupError,
shouldNavigateToWallet = action.shouldNavigateToWallet,
)
is SaveWalletAction.AllowToUseBiometrics.Success,
is SaveWalletAction.AllowToUseBiometrics.Error,
is SaveWalletAction.ProvideBackupInfo,
is SaveWalletAction.CloseError,
is SaveWalletAction.EnrollBiometrics,
is SaveWalletAction.EnrollBiometrics.Cancel,
-> Unit
}
}
private fun saveWalletAfterBackup(
state: SaveWalletState,
hasBackupError: Boolean,
shouldNavigateToWallet: Boolean,
) {
scope.launch {
val backupInfo = state.backupInfo ?: error("Backup info is null")
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory)
.create(scanResponse = backupInfo.scanResponse)
val userWallet = userWalletBuilder
.backupCardsIds(state.backupInfo.backupCardsIds)
.hasBackupError(hasBackupError)
.build()
.guard {
Timber.e("User wallet not created")
return@launch
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet, canOverride = true)
.flatMap {
saveAccessCodeIfNeeded(accessCode = backupInfo.accessCode, cardsInWallet = userWallet.cardsInWallet)
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
store.onUserWalletSelected(userWallet)
if (!shouldNavigateToWallet) {
store.dispatchWithMain(OnboardingWalletAction.WalletSaved(userWallet.walletId))
}
}
.doOnResult { if (shouldNavigateToWallet) navigateToWallet() }
}
}
private fun enrollBiometrics() {
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
}
private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch {
if (tangemSdkManager.checkNeedEnrollBiometrics()) {
store.dispatchWithMain(SaveWalletAction.EnrollBiometrics)
return@launch
}
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
} else {
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
state = com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On,
),
)
}
/*
* because it will be automatically saved on UserWalletsListManager switch
*/
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
val error = IllegalStateException("No selected user wallet")
Timber.e(error, "Unable to save user wallet")
store.dispatchWithMain(
SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)),
)
return@launch
}
handleSuccessAllowing(selectedUserWallet)
}.saveIn(saveWalletJobHolder)
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true)
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
}
private fun dismiss(state: SaveWalletState) {
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off))
} else {
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off,
),
)
}
}
private suspend fun saveAccessCodeIfNeeded(
accessCode: String?,
cardsInWallet: Set<String>,
): CompletionResult<Unit> {
return when {
accessCode.isNullOrBlank() -> {
CompletionResult.Success(Unit)
}
else -> {
tangemSdkManager.saveAccessCode(
accessCode = accessCode,
cardsIds = cardsInWallet,
)
}
}
}
private fun navigateToWallet() {
store.dispatchNavigationAction {
replaceAll(AppRoute.Wallet)
}
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
internal object SaveWalletReducer {
fun reduce(action: Action, state: AppState): SaveWalletState {
return if (action is SaveWalletAction) {
internalReduce(action, state.saveWalletState)
} else {
state.saveWalletState
}
}
private fun internalReduce(action: SaveWalletAction, state: SaveWalletState): SaveWalletState {
return when (action) {
is SaveWalletAction.ProvideBackupInfo -> state.copy(
backupInfo = SaveWalletState.WalletBackupInfo(
scanResponse = action.scanResponse,
accessCode = action.accessCode,
backupCardsIds = action.backupCardsIds,
),
)
is SaveWalletAction.AllowToUseBiometrics,
-> state.copy(isSaveInProgress = true)
is SaveWalletAction.AllowToUseBiometrics.Error -> state.copy(
error = action.error,
isSaveInProgress = false,
)
is SaveWalletAction.AllowToUseBiometrics.Success -> state.copy(
backupInfo = null,
isSaveInProgress = false,
)
is SaveWalletAction.Dismiss -> state.copy(
backupInfo = null,
)
is SaveWalletAction.CloseError -> state.copy(
error = null,
)
is SaveWalletAction.EnrollBiometrics -> state.copy(
needEnrollBiometrics = true,
)
is SaveWalletAction.EnrollBiometrics.Enroll,
is SaveWalletAction.EnrollBiometrics.Cancel,
-> state.copy(
needEnrollBiometrics = false,
isSaveInProgress = false,
)
is SaveWalletAction.SaveWalletAfterBackup,
-> state
}
}
}

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