Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 12:25:25 +02:00
commit 04a0d09058
1111 changed files with 35423 additions and 15304 deletions

View file

@ -14,6 +14,11 @@ android {
testOptions {
animationsDisabled = true
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
configurations.all {
@ -22,20 +27,12 @@ configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(module("com.facebook.react:react-android:0.72.4"))
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
substitute(module("org.bouncycastle:bcprov-jdk15on"))
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
}
force(
"org.bouncycastle:bcpkix-jdk15on:1.70",
"com.facebook.react:react-android:0.72.4",
"com.facebook.react:hermes-android:0.72.4",
)
}
}
@ -69,8 +66,10 @@ dependencies {
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.staking)
implementation(projects.domain.walletConnect)
implementation(projects.domain.markets)
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
@ -103,6 +102,7 @@ dependencies {
implementation(projects.data.qrScanning)
implementation(projects.data.staking)
implementation(projects.data.walletConnect)
implementation(projects.data.markets)
/** Features */
implementation(projects.features.onboarding)
@ -130,6 +130,14 @@ dependencies {
implementation(projects.features.staking.impl)
implementation(projects.features.details.api)
implementation(projects.features.details.impl)
implementation(projects.features.disclaimer.api)
implementation(projects.features.disclaimer.impl)
implementation(projects.features.pushNotifications.api)
implementation(projects.features.pushNotifications.impl)
implementation(projects.features.walletSettings.api)
implementation(projects.features.walletSettings.impl)
implementation(projects.features.markets.api)
implementation(projects.features.markets.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@ -166,6 +174,7 @@ dependencies {
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
implementation(deps.firebase.messaging)
/** Tangem libraries */
implementation(deps.tangem.blockchain) {
@ -207,10 +216,6 @@ dependencies {
implementation(deps.walletConnectCore)
implementation(deps.walletConnectWeb3)
implementation(deps.prettyLogger)
implementation("com.facebook.react:react-android:0.72.4")
implementation(deps.sprClient) {
exclude(group = "com.github.stephenc.jcip")
}
/** Testing libraries */
testImplementation(deps.test.coroutine)

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="resizeable_activity">true</bool>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="resizeable_activity">true</bool>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="resizeable_activity">true</bool>
</resources>

View file

@ -41,11 +41,11 @@
android:fullBackupContent="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:resizeableActivity="@bool/resizeable_activity"
android:label="@string/tangem_app_name"
android:largeHeap="@bool/largeHeap"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
@ -147,5 +147,19 @@
android:resource="@xml/provider_paths" />
</provider>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_tangem_24" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/white" />
<service
android:name="com.tangem.tap.common.pushes.TangemPushNotificationService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

@ -1 +1 @@
Subproject commit 35af912b96ce0f871906061bd483d9cf3af7451d
Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef

View file

@ -1,8 +1,32 @@
package com.tangem.tap
import android.content.Intent
import android.hardware.biometrics.BiometricManager
import android.os.Build
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
interface ActivityResultCaller {
val activityResultLauncher: ActivityResultLauncher<Intent>?
}
internal fun ActivityResultCaller.openSystemBiometrySettings() {
val settingsAction = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
Settings.ACTION_BIOMETRIC_ENROLL
}
else -> {
Settings.ACTION_SECURITY_SETTINGS
}
}
val intent = Intent(settingsAction).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BiometricManager.Authenticators.BIOMETRIC_STRONG,
)
}
}
activityResultLauncher?.launch(intent)
}

View file

@ -2,8 +2,11 @@ package com.tangem.tap
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
@ -15,6 +18,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -26,9 +30,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsEntryPoint
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
@ -59,8 +62,6 @@ interface ApplicationEntryPoint {
fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository
fun getManageTokensFeatureToggles(): ManageTokensFeatureToggles
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
@ -109,5 +110,13 @@ interface ApplicationEntryPoint {
fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getDetailsEntryPoint(): DetailsEntryPoint
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener
fun getShareManager(): ShareManager
fun getAppRouter(): AppRouter
fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles
}

View file

@ -0,0 +1,71 @@
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.message.EventMessageEffect
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(mode = 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)
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = uiDependencies.globalSnackbarHostState,
)
}
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

@ -3,13 +3,11 @@ package com.tangem.tap
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.time.Duration
@ -50,7 +48,7 @@ internal class LockUserWalletsTimer(
start()
if (shouldOpenWelcomeScreenOnResume) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
}
@ -128,7 +126,7 @@ internal class LockUserWalletsTimer(
if (wasApplicationStopped) {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
} else {
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome))
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
}

View file

@ -1,37 +1,38 @@
package com.tangem.tap
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.SystemBarStyle
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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
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.observe
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.AppRouter
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
@ -40,15 +41,20 @@ import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -59,6 +65,9 @@ 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.inject
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -68,17 +77,18 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.lang.ref.WeakReference
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@ -125,9 +135,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
@Inject
lateinit var manageTokensUi: ManageTokensUi
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@ -143,6 +150,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@ -158,6 +168,31 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var emailSender: EmailSender
@Inject
lateinit var stakingRouter: StakingRouter
@Inject
@RootAppComponentContext
internal lateinit var rootComponentContext: AppComponentContext
@Inject
internal lateinit var appRouterConfig: AppRouterConfig
@Inject
internal lateinit var routingComponentFactory: RoutingComponent.Factory
@Inject
internal lateinit var appRouter: AppRouter
@Inject
lateinit var pushNotificationsRouter: PushNotificationsRouter
@Inject
lateinit var cardRepository: CardRepository
@Inject
lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -177,6 +212,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val splashScreen = installSplashScreen()
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
Color.Transparent.toArgb(),
Color.Transparent.toArgb(),
),
)
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
@ -185,17 +227,43 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
observeAppThemeModeUpdates()
setContentView(R.layout.activity_main)
installRouting()
initContent()
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
sendStakingUnsubmittedHashes()
if (intent != null) {
deepLinksRegistry.launch(intent)
}
}
private fun installRouting() {
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
)
appRouterConfig.routerScope = lifecycleScope
appRouterConfig.componentRouter = routingComponent.router
routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack ->
appRouterConfig.stack = childStack.backStack
.plus(childStack.active)
.map { it.configuration }
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)
}
}
}
}
private fun observeStateUpdates() {
viewModel.state
.flowWithLifecycle(lifecycle)
@ -217,8 +285,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun installActivityDependencies() {
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
@ -238,11 +304,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
walletRouter = walletRouter,
walletConnectInteractor = walletConnectInteractor,
tokenDetailsRouter = tokenDetailsRouter,
manageTokensUi = manageTokensUi,
cardSdkConfigRepository = cardSdkConfigRepository,
sendRouter = sendRouter,
qrScanningRouter = qrScanningRouter,
emailSender = emailSender,
stakingRouter = stakingRouter,
pushNotificationsRouter = pushNotificationsRouter,
),
)
}
@ -263,8 +330,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@SuppressLint("SourceLockedOrientationActivity")
private fun initContent() {
WindowCompat.setDecorFitsSystemWindows(window, false)
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true,
@ -308,7 +373,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
override fun onDestroy() {
store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentProcessor.removeAll()
super.onDestroy()
}
@ -448,8 +512,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStack = store.state.navigationState.backStack
val isOnInitialScreen = backStack.all { it == AppScreen.Welcome || it == AppScreen.Home }
val backStack = appRouter.stack
val isOnInitialScreen = backStack.all { it is AppRoute.Welcome || it is AppRoute.Home }
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
@ -466,17 +530,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.Welcome,
bundle = intentWhichStartedActivity?.let {
bundleOf(WelcomeFragment.INITIAL_INTENT_KEY to it)
},
),
)
store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
}
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
lifecycleScope.launch {
val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
val isPushPermissionEnabled = toggles.isPushNotificationsEnabled
val shouldShowTos = !cardRepository.isTangemTOSAccepted() && isPushPermissionEnabled
val wasPushInitiallyAsked = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val shouldShowInitialPush = wasPushInitiallyAsked && isPushPermissionEnabled
val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)
shouldShowInitialPush -> AppRoute.PushNotification
else -> AppRoute.Home
}
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(intentWhichStartedActivity, false)
}
}
@ -484,16 +555,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}
private fun checkForNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
BuildConfig.LOG_ENABLED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
}
}
private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch {
getPolkadotCheckHasResetUseCase()
@ -514,4 +575,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
}
private fun sendStakingUnsubmittedHashes() {
lifecycleScope.launch {
sendUnsubmittedHashesUseCase.invoke()
.onRight { Timber.d("Submitting hashes succeeded") }
.onLeft { Timber.e(it.toString()) }
}
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
@ -32,6 +33,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -43,17 +45,15 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsEntryPoint
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.log.TimberFormatStrategy
@ -110,9 +110,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val walletConnectSessionsRepository: WalletConnectSessionsRepository
get() = entryPoint.getWalletConnectSessionsRepository()
private val manageTokensFeatureToggles: ManageTokensFeatureToggles
get() = entryPoint.getManageTokensFeatureToggles()
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
@ -181,13 +178,25 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase()
// endregion
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles()
private val detailsEntryPoint: DetailsEntryPoint
get() = entryPoint.getDetailsEntryPoint()
private val urlOpener
get() = entryPoint.getUrlOpener()
private val shareManager
get() = entryPoint.getShareManager()
private val appRouter: AppRouter
get() = entryPoint.getAppRouter()
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles
get() = entryPoint.getPushNotificationsFeatureToggles()
// endregion
override fun onCreate() {
super.onCreate()
@ -253,7 +262,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
customTokenFeatureToggles = customTokenFeatureToggles,
walletConnectRepository = walletConnect2Repository,
walletConnectSessionsRepository = walletConnectSessionsRepository,
manageTokensFeatureToggles = manageTokensFeatureToggles,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
@ -274,9 +282,14 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
detailsFeatureToggles = detailsFeatureToggles,
detailsEntryPoint = detailsEntryPoint,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener,
shareManager = shareManager,
appRouter = appRouter,
pushNotificationsFeatureToggles = pushNotificationsFeatureToggles,
),
),
)
@ -302,7 +315,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
initAnalytics(this, config)
initFeedbackManager(this, foregroundActivityObserver, store)
initFeedbackManager(this, store)
}
private fun initAnalytics(application: Application, config: Config) {
@ -323,11 +336,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA]
}
private fun initFeedbackManager(
context: Context,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {
private fun initFeedbackManager(context: Context, store: Store<AppState>) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo {
return AdditionalFeedbackInfo().apply {
appVersion = try {
@ -365,10 +374,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector,
)
val feedbackManager = FeedbackManager(
val feedbackManager = LegacyFeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(foregroundActivityObserver),
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
)

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.*
@ -46,7 +46,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(
context = context,
source = state.dialog.source,
onTryAgain = state.dialog.onTryAgain,
)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
/**
[REDACTED_AUTHOR]
*/
internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
override fun setContext(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
override fun eraseContext() {
Analytics.eraseContext()
}
override fun addContext(scanResponse: ScanResponse) {
Analytics.addContext(scanResponse)
}
override fun removeContext() {
Analytics.removeContext()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSources) : AnalyticsEvent(
category = "Cant Scan The Card",
event = button.event,
params = mapOf(
AnalyticsParam.SOURCE to source.value,
),
) {
enum class Buttons(val event: String) {
TRY_AGAIN("Try again button"),
HOW_TO_SCAN("Button blog"),
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.common.chat
import android.content.Context
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import java.io.File
class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver)
}
}
opener.open(createFeedbackFile, createLogsFile)
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.common.chat.opener
import android.content.Context
import java.io.File
internal interface ChatOpener {
fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?)
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.common.chat.opener.implementation
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.provider.Settings
import com.spr.messengerclient.config.SPRMessenger
import com.spr.messengerclient.config.bean.SPRMessengerConfig
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import timber.log.Timber
import java.io.File
import java.util.Locale
internal class SprinklrChatOpener(
private val config: SprinklrConfig,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
val messenger = SPRMessenger.shared()
if (messenger.config == null) {
initSprConfig(messenger)
}
messenger.startApplication()
}
private fun initSprConfig(messenger: SPRMessenger) {
val application = foregroundActivityObserver.foregroundActivity?.application.guard {
Timber.e("The SPR chat cannot be opened because there are no activities in foreground")
return
}
messenger.takeOff(application, createSprConfig(application, config))
}
@SuppressLint("HardwareIds")
private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig {
return SPRMessengerConfig().apply {
appId = config.appId
appKey = CHAT_APP_KEY
deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
environment = config.environment
skin = CHAT_SKIN
locale = Locale.getDefault().language
}
}
private companion object {
const val CHAT_APP_KEY = "com.sprinklr.messenger.release"
const val CHAT_SKIN = "MODERN"
}
}

View file

@ -1,171 +1,60 @@
package com.tangem.tap.common.extensions
import android.os.Bundle
import androidx.fragment.app.*
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.FragmentShareTransition
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
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 FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
bundle: Bundle? = null,
fgShareTransition: FragmentShareTransition? = null,
) {
val transaction = supportFragmentManager.beginTransaction().apply {
setReorderingAllowed(true)
}
fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider<Fragment>) {
if (backStackEntryCount > 0) {
val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name
val fragment = fragmentFactory(screen).apply {
arguments = bundle
if (fgShareTransition != null) {
sharedElementEnterTransition = fgShareTransition.enterTransitionSet
sharedElementReturnTransition = fgShareTransition.exitTransitionSet
fgShareTransition.shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.elementName)
}
}
if (name == currentFragmentName) {
Timber.d("Fragment $name is already at the top of the stack")
return
}
}
if (screen.isDialogFragment && fragment is DialogFragment) {
fragment.showAllowingStateLoss(
fragmentManager = supportFragmentManager,
baseTransaction = transaction,
tag = screen.name,
addToBackstack = addToBackstack,
)
Timber.d("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.d("Route $name is shown")
} else {
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack) {
transaction.addToBackStack(screen.name)
}
Timber.d("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 DialogFragment.showAllowingStateLoss(
fragmentManager: FragmentManager,
baseTransaction: FragmentTransaction,
tag: String,
addToBackstack: Boolean,
) {
runCatching {
if (addToBackstack) baseTransaction.addToBackStack(tag)
show(baseTransaction, tag)
}
.onFailure { throwable ->
if (throwable is IllegalStateException) {
val transaction = fragmentManager.beginTransaction()
transaction.add(this, tag)
if (addToBackstack) transaction.addToBackStack(tag)
private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
transaction.commitAllowingStateLoss()
} else {
Timber.e(throwable)
}
}
}
transaction.replace(R.id.fragment_container, this, name)
transaction.addToBackStack(name)
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
try {
this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag)
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
fun FragmentActivity.getPreviousScreen(): AppScreen? {
val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) {
this.supportFragmentManager.backStackEntryCount - 1
} else {
0
}
val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount) {
this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
} else {
null
}
return tag?.let { AppScreen.valueOf(tag) }
}
@Suppress("ComplexMethod", "LongMethod")
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> {
store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment()
}
AppScreen.Send -> {
val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles)
if (featureToggles.isRedesignedSendEnabled) {
store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment()
} else {
SendFragment()
}
}
AppScreen.Details -> {
val featureToggles = store.inject(getDependency = DaggerGraphState::detailsFeatureToggles)
if (featureToggles.isRedesignEnabled) {
store.inject(DaggerGraphState::detailsEntryPoint).entryFragment()
} else {
DetailsFragment()
}
}
AppScreen.DetailsSecurity -> SecurityModeFragment()
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.ManageTokens -> TokensListFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> {
store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment()
}
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScanning -> {
store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment()
}
AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Swap -> SwapFragment()
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
}
transaction.commitAllowingStateLoss()
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.common.extensions
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.common.routing.AppRouter
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -82,12 +82,16 @@ suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
fun Store<*>.dispatchOpenUrl(url: String) {
dispatch(NavigationAction.OpenUrl(url))
fun Store<AppState>.dispatchOpenUrl(url: String) {
inject(DaggerGraphState::urlOpener).openUrl(url)
}
fun Store<*>.dispatchShare(url: String) {
dispatch(NavigationAction.Share(url))
fun Store<AppState>.dispatchShare(url: String) {
inject(DaggerGraphState::shareManager).shareText(url)
}
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
inject(DaggerGraphState::appRouter).action()
}
inline fun <reified T> Store<AppState>.inject(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -3,7 +3,10 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.*
@ -78,16 +81,6 @@ fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
return clipData.getItemAt(0).text
}
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, *formatArgs)
}

View file

@ -2,18 +2,17 @@ package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.chat.ChatManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver
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.withForegroundActivity
import kotlinx.coroutines.launch
@ -25,27 +24,33 @@ import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
class LegacyFeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val chatManager: ChatManager,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase,
) {
private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) {
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
mainScope.launch {
scope.launch {
val getCardInfo = suspend {
scanResponse ?: error("ScanResponse must be not null")
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
}
val email = getFeedbackEmailUseCase(
when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter
type = when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo())
is ScanFailsEmail -> FeedbackEmailType.ScanningProblem
is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem
else -> FeedbackEmailType.DirectUserRequest
is SendTransactionFailedEmail -> {
FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo())
}
else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
},
)
@ -66,46 +71,25 @@ class FeedbackManager(
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity),
onFail = onFail,
)
}
}
}
fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
chatManager.open(
config = config,
createLogsFile = ::getLogFile,
createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) },
)
}
fun sendEmail(type: FeedbackEmailType) {
if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled")
private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? {
return try {
if (sessionFeedbackFile != null) {
return sessionFeedbackFile
}
val file = File(context.filesDir, FEEDBACK_FILE)
file.delete()
file.createNewFile()
scope.launch {
val email = getFeedbackEmailUseCase(type = type)
val feedback = feedbackData.run {
prepare(infoHolder)
joinTogether(context, infoHolder)
}
val fileWriter = FileWriter(file)
fileWriter.write(feedback)
fileWriter.close()
if (file.exists()) {
sessionFeedbackFile = file
sessionFeedbackFile
} else {
null
}
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
store.inject(DaggerGraphState::emailSender).send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
}
@ -144,10 +128,9 @@ class FeedbackManager(
}
}
companion object {
private companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
const val FEEDBACK_FILE = "feedback.txt"
const val LOGS_FILE = "logs.txt"
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common.feedback
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.store
import timber.log.Timber
internal class ProxyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackEmailType) {
val manager = store.state.globalState.feedbackManager
if (manager == null) {
Timber.e("Feedback manager is not initialized")
return
}
manager.sendEmail(type)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.common.finisher
import android.content.Context
import android.content.Intent
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.tap.MainActivity
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
internal class AndroidAppFinisher(
private val appContext: Context,
) : AppFinisher {
override fun finish() {
foregroundActivityObserver.withForegroundActivity { activity ->
activity.finish()
}
}
override fun restart() {
val intent = Intent(appContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
appContext.startActivity(intent)
Runtime.getRuntime().exit(0)
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.wallet.R
import timber.log.Timber
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
// TODO refactoring: [REDACTED_JIRA]
val intent = Intent(applicationContext, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
val notificationBuilder =
NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(notification.title)
.setContentText(notification.body)
.setPriority(message.priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(notification.vibrateTimings)
.apply {
notification.imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(applicationContext, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
applicationContext,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(applicationContext)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.StateDialog
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

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.common.redux.navigation.NavigationReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
@ -23,7 +22,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state, appStateHolder),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),

View file

@ -1,13 +1,11 @@
package com.tangem.tap.common.redux
import com.tangem.core.navigation.NavigationState
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.NetworkServices
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
@ -40,7 +38,6 @@ import org.rekotlin.Middleware
import org.rekotlin.StateType
data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
@ -73,7 +70,6 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware,
navigationMiddleware,
notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,

View file

@ -2,21 +2,19 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -75,14 +73,12 @@ sealed class GlobalAction : Action {
) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : GlobalAction() {

View file

@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
@ -69,27 +69,10 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
}
is GlobalAction.OpenChat -> {
val globalState = store.state.globalState
val feedbackManager = globalState.feedbackManager.guard {
store.dispatchDebugErrorNotification("FeedbackManager not initialized")
return
}
val config = globalState.configManager?.config.guard {
store.dispatchDebugErrorNotification("Config not initialized")
return
}
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: config.sprinklr
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("The chat config is not initialized")
return
}
feedbackManager.openChat(chatConfig, action.feedbackData)
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = action.feedbackData,
scanResponse = action.scanResponse,
)
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return

View file

@ -1,11 +1,11 @@
package com.tangem.tap.common.redux.global
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.onboarding.OnboardingManager
@ -20,7 +20,7 @@ data class GlobalState(
val tapWalletManager: TapWalletManager = TapWalletManager(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val feedbackManager: LegacyFeedbackManager? = null,
val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.redux.legacy
import com.tangem.blockchain.common.AmountType
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@ -20,10 +23,16 @@ internal object LegacyMiddleware {
{ action ->
when (action) {
is LegacyAction.SendEmailRateCanBeBetter -> {
store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail())
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = RateCanBeBetterEmail(),
scanResponse = action.scanResponse,
)
}
is LegacyAction.SendEmailSupport -> {
store.state.globalState.feedbackManager?.sendEmail(FeedbackEmail())
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = FeedbackEmail(),
scanResponse = action.scanResponse,
)
}
is LegacyAction.StartOnboardingProcess -> {
store.dispatch(
@ -32,8 +41,28 @@ internal object LegacyMiddleware {
}
is LegacyAction.SendEmailTransactionFailed -> {
if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
val amount = action.amount?.convertToAmount(action.cryptoCurrency)
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
error = BlockchainErrorInfo(
errorMessage = action.errorMessage,
blockchainId = action.cryptoCurrency.network.id.value,
derivationPath = action.cryptoCurrency.network.derivationPath.value,
destinationAddress = action.destinationAddress.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
""
},
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
fee = action.fee?.convertToAmount(action.cryptoCurrency)
?.value?.stripZeroPlainString() ?: "unknown",
),
)
store.state.globalState.feedbackManager?.sendEmail(
SendTransactionFailedEmail(action.errorMessage),
feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = action.scanResponse,
)
} else {
scope.launch {
@ -50,7 +79,8 @@ internal object LegacyMiddleware {
)
}
store.state.globalState.feedbackManager?.sendEmail(
SendTransactionFailedEmail(action.errorMessage),
feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = null,
)
}
}

View file

@ -1,91 +0,0 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
import android.hardware.biometrics.BiometricManager
import android.os.Build
import android.provider.Settings
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
val navigationMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
val navState = state()?.navigationState
when (action) {
is NavigationAction.NavigateTo -> {
navState?.activity?.get()?.openFragment(
screen = action.screen,
addToBackstack = action.addToBackstack,
fgShareTransition = action.fragmentShareTransition,
bundle = action.bundle,
)
}
is NavigationAction.PopBackTo -> {
if (navState?.backStack?.lastOrNull() != action.screen) {
when (val screen = action.screen) {
AppScreen.Home,
AppScreen.Welcome,
-> {
if (navState?.backStack?.contains(screen) == false) {
// Pop back to activity
navState.activity?.get()?.popBackTo(screen = null, inclusive = true)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
} else {
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
}
}
else -> {
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
}
}
}
}
is NavigationAction.OpenUrl -> {
navState?.activity?.get()?.let {
CustomTabsManager().openUrl(action.url, it)
}
}
is NavigationAction.OpenDocument -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog)
is NavigationAction.OpenBiometricsSettings -> {
val settingsAction = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
Settings.ACTION_BIOMETRIC_ENROLL
}
else -> {
Settings.ACTION_SECURITY_SETTINGS
}
}
val intent = Intent(settingsAction).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BiometricManager.Authenticators.BIOMETRIC_STRONG,
)
}
}
activityResultCaller.activityResultLauncher?.launch(intent)
}
is NavigationAction.Share -> {
navState?.activity?.get()?.shareText(action.data)
}
is NavigationAction.ActivityCreated,
is NavigationAction.ActivityDestroyed,
-> Unit
}
}
next(action)
}
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.tap.common.redux.navigation
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationState
import com.tangem.tap.common.extensions.getPreviousScreen
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object NavigationReducer {
fun reduce(action: Action, state: AppState): NavigationState = internalReduce(action, state)
}
private fun internalReduce(action: Action, state: AppState): NavigationState {
val navigationAction = action as? NavigationAction ?: return state.navigationState
val navState = state.navigationState
return when (navigationAction) {
is NavigationAction.NavigateTo -> {
navState.copy(backStack = navState.backStack + navigationAction.screen)
}
is NavigationAction.PopBackTo -> {
if (navState.backStack.lastOrNull() == navigationAction.screen) return navState
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
}
is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity)
is NavigationAction.ActivityDestroyed -> {
when {
// Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a
// new scan from the background [REDACTED_TASK_KEY]
navState.activity?.get() == navigationAction.activity.get() -> NavigationState()
else -> navState
}
}
else -> navState
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common.settings
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.provider.Settings
import com.tangem.core.navigation.settings.SettingsManager
internal class IntentSettingsManager(val context: Context) : SettingsManager {
override fun openSettings() {
val openSettingsIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null),
).apply {
flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
context.startActivity(openSettingsIntent)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.tap.common.share
import android.content.Intent
import com.tangem.core.navigation.share.ShareManager
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
internal class IntentShareManager : ShareManager {
override fun shareText(text: String) {
foregroundActivityObserver.withForegroundActivity { activity ->
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
activity.startActivity(shareIntent)
}
}
}

View file

@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
@ -44,7 +43,7 @@ class RussianCardholdersWarningBottomSheetDialog(
dismiss()
}
binding?.btnNo?.setOnClickListener {
store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL))
store.dispatchOpenUrl(INSTRUCTION_URL)
dismiss()
}
}

View file

@ -1,37 +1,76 @@
package com.tangem.tap.common.ui
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object ScanFailsDialog {
fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
val sourceAnalytics = when (source) {
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
}
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
internal object ScanFailsDialog {
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
val customView = View.inflate(context, R.layout.dialog_scan_fails, null)
val sourceAnalytics = when (source) {
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
}
setNeutralButton(R.string.common_cancel) { _, _ -> }
val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button)
if (onTryAgain != null) {
tryAgainBtn?.isVisible = true
tryAgainBtn?.setOnClickListener {
store.dispatchDialogHide()
Analytics.send(
ScanFailsDialogAnalytics(
button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN,
source = sourceAnalytics,
),
)
onTryAgain()
}
} else {
tryAgainBtn?.isVisible = false
}
customView.findViewById<TextView>(R.id.how_to_scan_button)?.setOnClickListener {
Analytics.send(
ScanFailsDialogAnalytics(
button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN,
source = sourceAnalytics,
),
)
val locale = LocaleRegionProvider().getRegion()
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link)
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
store.dispatch(GlobalAction.SendEmail(feedbackData = ScanFailsEmail(), scanResponse = null))
}
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
store.dispatchDialogHide()
}
setView(customView)
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common
package com.tangem.tap.common.url
import android.content.Context
import android.net.Uri
@ -6,12 +6,22 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
class CustomTabsManager {
fun openUrl(url: String, context: Context) {
internal class CustomTabsUrlOpener : UrlOpener {
override fun openUrl(url: String) {
foregroundActivityObserver.withForegroundActivity {
openUrl(url, context = it)
}
}
private fun openUrl(url: String, context: Context) {
if (url.isEmpty()) return
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(

View file

@ -1,9 +1,11 @@
package com.tangem.tap.data
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
@ -15,6 +17,9 @@ internal class RuntimeUserWalletsStore(
override val selectedUserWalletOrNull: UserWallet?
get() = userWalletsListManager.selectedUserWalletSync
override val userWallets: Flow<List<UserWallet>>
get() = userWalletsListManager.userWallets
override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListManager
.userWallets
@ -26,7 +31,10 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
userWalletsListManager.update(userWalletId, update)
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return userWalletsListManager.update(userWalletId, update)
}
}

View file

@ -6,10 +6,8 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.feature.tester.ActivityClassWrapper
import com.tangem.tap.MainActivity
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
@ -67,10 +65,4 @@ internal object ActivityModule {
): GetPolkadotCheckHasImmortalUseCase {
return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository)
}
@Provides
@Singleton
fun provideActivityClassWrapper(): ActivityClassWrapper {
return ActivityClassWrapper(MainActivity::class.java)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.tap.proxy.AppStateHolder
import dagger.Binds
@ -13,10 +12,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal interface AppStateHolderModule {
@Binds
@Singleton
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController
@Binds
@Singleton
fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder

View file

@ -7,8 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.decompose.ui.UiMessage
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.ui.UiDependencies
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -16,7 +15,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import timber.log.Timber
@Module
@InstallIn(ActivityComponent::class)
@ -29,17 +27,11 @@ internal object RootAppComponentContextModule {
@ActivityContext context: Context,
dispatchers: CoroutineDispatcherProvider,
componentBuilder: DecomposeComponent.Builder,
uiDependencies: UiDependencies,
): AppComponentContext {
// TODO: Implement message handler
val dummyMessageHandler = object : UiMessageHandler {
override fun handleMessage(message: UiMessage) {
Timber.w("Unable to handle message: $message")
}
}
return DefaultAppComponentContext(
componentContext = (context as AppCompatActivity).defaultComponentContext(),
messageHandler = dummyMessageHandler,
messageHandler = uiDependencies.eventMessageHandler,
dispatchers = dispatchers,
hiltComponentBuilder = componentBuilder,
)

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di
import androidx.compose.material3.SnackbarHostState
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder
import dagger.Module
import dagger.Provides
@ -19,6 +21,8 @@ internal object UiDependenciesModule {
return object : UiDependencies {
override val hapticManager = hapticManager
override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.tap.common.feedback.ProxyFeedbackManager
import com.tangem.tap.common.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager
import com.tangem.tap.common.url.CustomTabsUrlOpener
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UtilsModule {
@Provides
@Singleton
fun provideShareManager(): ShareManager = IntentShareManager()
@Provides
@Singleton
fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener()
@Provides
@Singleton
fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager()
@Provides
@Singleton
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
@Provides
@Singleton
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context)
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import dagger.Module
import dagger.Provides
@ -17,4 +19,8 @@ internal object AnalyticsModule {
fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase {
return DefaultChangeCardAnalyticsContextUseCase()
}
@Provides
@Singleton
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
}

View file

@ -84,4 +84,10 @@ internal object CardDomainModule {
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
return DefaultResetCardUseCase(tangemSdkManager)
}
@Provides
@Singleton
fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase {
return NetworkHasDerivationUseCase()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
@ -15,6 +16,12 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase {
return GetCardInfoUseCase(feedbackRepository = feedbackRepository)
}
@Provides
@Singleton
fun provideGetFeedbackEmailUseCase(

View file

@ -0,0 +1,22 @@
package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object MarketsDomainModule {
@Provides
@Singleton
fun provideGetMarketsTokenListFlowUseCase(
marketsTokenRepository: MarketsTokenRepository,
): GetMarketsTokenListFlowUseCase {
return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.*
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -17,6 +18,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object SettingsDomainModule {
@ -164,4 +166,36 @@ internal object SettingsDomainModule {
): IncrementAppLaunchCounterUseCase {
return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository)
}
// region PushPermissionRepository
@Provides
@Singleton
fun provideShouldInitiallyAskPermissionUseCase(
permissionRepository: PermissionRepository,
): ShouldInitiallyAskPermissionUseCase {
return ShouldInitiallyAskPermissionUseCase(repository = permissionRepository)
}
@Provides
@Singleton
fun provideNeverToInitiallyAskPermissionUseCase(
permissionRepository: PermissionRepository,
): NeverToInitiallyAskPermissionUseCase {
return NeverToInitiallyAskPermissionUseCase(repository = permissionRepository)
}
@Provides
@Singleton
fun provideShouldAskPermissionUseCase(permissionRepository: PermissionRepository): ShouldAskPermissionUseCase {
return ShouldAskPermissionUseCase(repository = permissionRepository)
}
@Provides
@Singleton
fun provideNeverRequestPermissionUseCase(
permissionRepository: PermissionRepository,
): NeverRequestPermissionUseCase {
return NeverRequestPermissionUseCase(repository = permissionRepository)
}
// endregion
}

View file

@ -1,8 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.settings.*
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import dagger.Module
import dagger.Provides
@ -16,17 +15,145 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase {
return GetStakingEntryInfoUseCase(
fun provideGetStakingAvailabilityUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingAvailabilityUseCase {
return GetStakingAvailabilityUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase {
return GetStakingAvailabilityUseCase(
fun provideGetStakingEntryInfoUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingEntryInfoUseCase {
return GetStakingEntryInfoUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetYieldUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetYieldUseCase {
return GetYieldUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetStakingTokensUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingTokensUseCase {
return FetchStakingTokensUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideFetchStakingYieldBalanceUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetStakingYieldBalanceUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingYieldBalanceUseCase {
return GetStakingYieldBalanceUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideInitializeStakingProcessUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingTransactionUseCase {
return GetStakingTransactionUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGasEstimateUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): EstimateGasUseCase {
return EstimateGasUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideSubmitHashUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): SubmitHashUseCase {
return SubmitHashUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideSaveUnsubmittedHashUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): SaveUnsubmittedHashUseCase {
return SaveUnsubmittedHashUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideSendUnsubmittedHashesUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): SendUnsubmittedHashesUseCase {
return SendUnsubmittedHashesUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsStakeMoreAvailableUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsStakeMoreAvailableUseCase {
return IsStakeMoreAvailableUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
}

View file

@ -2,10 +2,12 @@ package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -54,8 +56,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetTokenListUseCase {
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository)
return GetTokenListUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
)
}
@Provides
@ -64,8 +72,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository)
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository)
}
@Provides
@ -83,9 +92,16 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
return GetCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
dispatchers,
)
}
@Provides
@ -100,6 +116,7 @@ internal object TokensDomainModule {
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
@ -112,6 +129,7 @@ internal object TokensDomainModule {
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -122,12 +140,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
dispatchers,
)
}
@ -148,8 +168,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides
@ -190,6 +211,8 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
stakingFeatureToggles: StakingFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@ -199,6 +222,8 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
stakingFeatureToggles = stakingFeatureToggles,
dispatchers = dispatchers,
)
}
@ -209,12 +234,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetNetworkCoinStatusUseCase {
return GetNetworkCoinStatusUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -225,12 +252,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetFeePaidCryptoCurrencyStatusSyncUseCase {
return GetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -363,11 +392,13 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}

View file

@ -64,6 +64,23 @@ internal object TransactionDomainModule {
return CreateTransactionUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideCreateTransactionExtrasUseCase(
transactionRepository: TransactionRepository,
): CreateTransactionDataExtrasUseCase {
return CreateTransactionDataExtrasUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase {
return EstimateFeeUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig(),
)
}
@Provides
@Singleton
fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase {

View file

@ -1,12 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
@ -95,8 +95,11 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesRenameWalletUseCase(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers)
}
@Provides

View file

@ -0,0 +1,26 @@
package com.tangem.tap.di.routing
import com.tangem.common.routing.AppRouter
import com.tangem.tap.routing.ProxyAppRouter
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.configurator.MutableAppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AppRouterModule {
@Provides
@Singleton
fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter =
ProxyAppRouter(config, dispatchers)
@Provides
@Singleton
fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig()
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.routing
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.impl.DefaultRoutingComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal interface RoutingComponentModule {
@Binds
@ActivityScoped
fun bindRoutingComponentFactory(factory: DefaultRoutingComponent.Factory): RoutingComponent.Factory
}

View file

@ -1,13 +1,17 @@
package com.tangem.tap.domain.card
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
@ -15,7 +19,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.withContext
import timber.log.Timber
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
@ -44,10 +48,12 @@ internal class DefaultDerivationsRepository(
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold(
onSuccess = { return },
onFailure = { throw it },
)
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
return
}
.doOnFailure { throw it }
}
.doOnFailure { throw it }
@ -76,8 +82,24 @@ internal class DefaultDerivationsRepository(
}
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result<Unit> {
return runCatching(dispatchers.io) {
/**
* It throws an exception if any of the provided derivations are invalid
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
* It needs to be called after success [derivePublicKeys] or in same flows
*/
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
derivations.entries.forEach { derivationForKey ->
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
if (wallet == null) return@forEach
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
throw TangemSdkError.NonHardenedDerivationNotSupported()
}
}
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult<UserWallet> {
return withContext(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.updateDerivedKeys(keys) },

View file

@ -11,8 +11,8 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.card.models.ResetCardError
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -20,24 +20,25 @@ internal class DefaultResetCardUseCase(
private val tangemSdkManager: TangemSdkManager,
) : ResetCardUseCase {
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = resourceScope {
either {
withUserCodeRequestPolicy(card)
override suspend fun invoke(cardId: String, params: ResetCardUserCodeParams): Either<ResetCardError, Boolean> =
resourceScope {
either {
withUserCodeRequestPolicy(params)
tangemSdkManager.resetToFactorySettings(
cardId = card.cardId,
allowsRequestAccessCodeFromRepository = true,
).bind(raise = this)
tangemSdkManager.resetToFactorySettings(
cardId = cardId,
allowsRequestAccessCodeFromRepository = true,
).bind(raise = this)
}
}
}
override suspend fun invoke(
cardNumber: Int,
card: CardDTO,
params: ResetCardUserCodeParams,
userWalletId: UserWalletId,
): Either<ResetCardError, Unit> = resourceScope {
): Either<ResetCardError, Boolean> = resourceScope {
either {
withUserCodeRequestPolicy(card)
withUserCodeRequestPolicy(params)
tangemSdkManager.resetBackupCard(
cardNumber = cardNumber,
@ -46,11 +47,11 @@ internal class DefaultResetCardUseCase(
}
}
private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
private suspend fun ResourceScope.withUserCodeRequestPolicy(params: ResetCardUserCodeParams) {
install(
acquire = {
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
requestMandatoryAccessCodeEntry(card)
requestMandatoryAccessCodeEntry(params)
policyBeforeReset
},
@ -60,10 +61,10 @@ internal class DefaultResetCardUseCase(
)
}
private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
val type = if (card.isAccessCodeSet) {
private fun requestMandatoryAccessCodeEntry(params: ResetCardUserCodeParams) {
val type = if (params.isAccessCodeSet) {
UserCodeType.AccessCode
} else if (card.isPasscodeSet == true) {
} else if (params.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
@ -74,15 +75,14 @@ internal class DefaultResetCardUseCase(
}
}
private fun CompletionResult<*>.bind(raise: Raise<ResetCardError>) {
private fun CompletionResult<Boolean>.bind(raise: Raise<ResetCardError>): Boolean {
return when (this) {
is CompletionResult.Failure -> {
val domainError = error.mapToDomainError()
raise.raise(domainError)
}
is CompletionResult.Success -> { /* no-op */
}
is CompletionResult.Success -> data
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
data class PendingTransaction(
val transactionData: TransactionData,
val transactionData: TransactionData.Uncompiled,
val type: PendingTransactionType,
) {
val address: String? = when (type) {
@ -21,7 +21,7 @@ data class PendingTransaction(
enum class PendingTransactionType { Incoming, Outgoing, Unknown }
fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransaction? {
fun TransactionData.Uncompiled.toPendingTransaction(walletAddress: String): PendingTransaction? {
if (this.status == TransactionStatus.Confirmed) return null
val type: PendingTransactionType = when {
@ -32,7 +32,7 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact
return PendingTransaction(this, type)
}
fun List<TransactionData>.toPendingTransactions(walletAddress: String): List<PendingTransaction> {
fun List<TransactionData.Uncompiled>.toPendingTransactions(walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransaction(walletAddress) }
}

View file

@ -5,12 +5,11 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
@ -21,6 +20,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
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
@ -32,6 +32,7 @@ import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal object LegacyScanProcessor {
@ -115,24 +116,28 @@ internal object LegacyScanProcessor {
} else {
scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
disclaimerWillShow()
store.dispatchWithMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onFailure(TangemSdkError.UserCancelled())
}
},
withContext(Dispatchers.Main.immediate) {
disclaimerWillShow()
store.dispatch(
DisclaimerAction.Show(
from = DisclaimerSource.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main.immediate) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main.immediate) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
),
)
)
}
}
}
}
@ -170,7 +175,7 @@ internal object LegacyScanProcessor {
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
@ -178,9 +183,9 @@ internal object LegacyScanProcessor {
}
}
private suspend inline fun navigateTo(screen: AppScreen, onProgressStateChange: (showProgress: Boolean) -> Unit) {
private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
store.dispatchNavigationAction { push(route) }
onProgressStateChange(false)
}
}

View file

@ -3,16 +3,15 @@ package com.tangem.tap.domain.scanCard
import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
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.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
@ -60,7 +59,11 @@ internal object UseCaseScanProcessor {
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
add(DisclaimerChain(store, disclaimerWillShow))
val pushNotificationsToggles =
store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
if (pushNotificationsToggles.isPushNotificationsEnabled) {
add(DisclaimerChain(store, disclaimerWillShow))
}
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
@ -132,8 +135,8 @@ internal object UseCaseScanProcessor {
action()
}
private suspend inline fun navigateTo(screen: AppScreen) {
private suspend inline fun navigateTo(route: AppRoute) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
store.dispatchNavigationAction { push(route) }
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.tap.domain.scanCard.chains
import arrow.core.left
import arrow.core.right
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
@ -55,8 +55,8 @@ class CheckForOnboardingChain(
canSkipBackup = previousChainResult.card.canSkipBackup,
),
)
val appScreen = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(appScreen).left()
val route = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(route).left()
}
else -> {
Analytics.setContext(previousChainResult)
@ -69,7 +69,7 @@ class CheckForOnboardingChain(
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppScreen.OnboardingTwins).left()
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard.chains
import arrow.core.left
import arrow.core.right
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
@ -13,6 +12,7 @@ import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import kotlinx.coroutines.suspendCancellableCoroutine
import org.rekotlin.Store
import kotlin.coroutines.resume
@ -50,7 +50,7 @@ internal class DisclaimerChain(
return suspendCancellableCoroutine { continuation ->
store.dispatchOnMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
from = DisclaimerSource.Home,
callback = DisclaimerCallback(
onAccept = {
if (continuation.isActive) {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain.scanCard.chains
import com.tangem.core.navigation.AppScreen
import com.tangem.common.routing.AppRoute
import com.tangem.domain.card.ScanCardException
sealed class ScanChainException : ScanCardException.ChainException() {
@ -19,5 +19,5 @@ sealed class ScanChainException : ScanCardException.ChainException() {
*
* @param onboardingRoute route where to navigate
* */
data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException()
data class OnboardingNeeded(val onboardingRoute: AppRoute) : ScanChainException()
}

View file

@ -70,9 +70,9 @@ interface TangemSdkManager {
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO>
): CompletionResult<Boolean>
suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit>
suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean>
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit>

View file

@ -202,7 +202,7 @@ class DefaultTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
): CompletionResult<Boolean> {
return runTaskAsyncReturnOnMain(
runnable = ResetToFactorySettingsTask(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
@ -210,10 +210,9 @@ class DefaultTangemSdkManager(
cardId = cardId,
initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)),
)
.map { CardDTO(it) }
}
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit> {
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean> {
return runTaskAsyncReturnOnMain(
runnable = ResetBackupCardTask(userWalletId),
initialMessage = Message(

View file

@ -87,12 +87,12 @@ class MockTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return MockProvider.getCardDto()
): CompletionResult<Boolean> {
return CompletionResult.Success(true)
}
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean> {
return CompletionResult.Success(true)
}
override suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {

View file

@ -19,11 +19,11 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*/
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
) : CardSessionRunnable<Unit> {
) : CardSessionRunnable<Boolean> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(session: CardSession, callback: CompletionCallback<Unit>) {
override fun run(session: CardSession, callback: CompletionCallback<Boolean>) {
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId),
@ -35,10 +35,10 @@ internal class ResetBackupCardTask(
}
}
private fun resetCard(session: CardSession, callback: CompletionCallback<Unit>) {
private fun resetCard(session: CardSession, callback: CompletionCallback<Boolean>) {
ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(Unit))
is CompletionResult.Success -> callback(CompletionResult.Success(result.data))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -10,13 +10,15 @@ import com.tangem.operations.wallet.PurgeWalletCommand
class ResetToFactorySettingsTask(
override val allowsRequestAccessCodeFromRepository: Boolean,
) : CardSessionRunnable<Card> {
) : CardSessionRunnable<Boolean> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private var isResetCompleted = false
override fun run(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
deleteWallets(session, callback)
}
private fun deleteWallets(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private fun deleteWallets(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
val wallet = session.environment.card?.wallets?.lastOrNull().guard {
resetBackup(session, callback)
return
@ -25,6 +27,7 @@ class ResetToFactorySettingsTask(
PurgeWalletCommand(wallet.publicKey).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
isResetCompleted = true
deleteWallets(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
@ -32,18 +35,18 @@ class ResetToFactorySettingsTask(
}
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Boolean>) -> Unit) {
if (session.environment.card?.backupStatus == null ||
session.environment.card?.backupStatus == Card.BackupStatus.NoBackup
) {
callback(CompletionResult.Success(session.environment.card!!))
callback(CompletionResult.Success(isResetCompleted))
return
}
ResetBackupCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(session.environment.card!!))
callback(CompletionResult.Success(true))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}

View file

@ -91,9 +91,8 @@ class WalletConnectSdkHelper {
val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" }
val transactionData = TransactionData(
val transactionData = TransactionData.Uncompiled(
amount = Amount(value, wallet.blockchain),
// TODO refactoring
fee = Fee.Common(Amount(fee, wallet.blockchain)),
sourceAddress = transaction.from,
destinationAddress = destinationAddress,

View file

@ -508,6 +508,6 @@ internal class DefaultLegacyWalletConnectRepository(
private companion object {
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro")
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
}
}

View file

@ -14,7 +14,8 @@ import androidx.lifecycle.Lifecycle
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -36,7 +37,7 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP
}
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.customtoken.impl.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -8,7 +9,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
@ -32,15 +32,12 @@ internal class AddCustomTokenFragment : ComposeFragment() {
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
val statusBarColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(color = statusBarColor)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AddCustomTokenScreen(
modifier = Modifier.systemBarsPadding(),
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
stateHolder = state,
)
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.customtoken.impl.presentation.routers
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -12,11 +14,11 @@ import com.tangem.wallet.R
internal class DefaultCustomTokenRouter : CustomTokenRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
override fun openWalletScreen() {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
override fun openUnsupportedNetworkAlert(blockchain: Blockchain) {
@ -32,7 +34,7 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter {
val alert = AppDialog.SimpleOkDialogRes(
headerId = R.string.common_error,
messageId = R.string.common_unknown_error,
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
)
store.dispatchDialogShow(alert)
}

View file

@ -4,7 +4,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
@ -21,7 +20,6 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)

View file

@ -14,26 +14,6 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
data object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
data object Start : ResetToFactory()
data object Proceed : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
data object Failure : ResetToFactory()
data object Success : ResetToFactory()
data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory()
data object DismissDialog : ResetToFactory()
}
data object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction()
data object ResetCardSettingsData : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
data object Success : DetailsAction()
@ -43,26 +23,6 @@ sealed class DetailsAction : Action {
data object DismissError : DetailsAction()
sealed class AccessCodeRecovery : DetailsAction() {
object Open : AccessCodeRecovery()
data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() {
data class Success(val enabled: Boolean) : AccessCodeRecovery()
}
data class SelectOption(val enabled: Boolean) : AccessCodeRecovery()
}
sealed class ManageSecurity : DetailsAction() {
data object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
data object SaveChanges : ManageSecurity() {
data object Success : ManageSecurity()
data object Failure : ManageSecurity()
}
data object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
@ -96,6 +56,8 @@ sealed class DetailsAction : Action {
data class ChangeAppCurrency(
val currency: AppCurrency,
) : AppSettings()
data class Prepare(val state: AppSettingsState) : AppSettings()
}
data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction()

View file

@ -1,40 +1,36 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
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.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
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 com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -42,17 +38,13 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val appSettingsMiddleware = AppSettingsMiddleware()
private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
@ -69,152 +61,11 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
is DetailsAction.ReCreateTwinsWallet -> {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
is DetailsAction.ScanCard -> scanCard(state)
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
}
}
class EraseWalletMiddleware {
@Suppress("CyclomaticComplexMethod")
fun handle(action: DetailsAction.ResetToFactory) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (action) {
is DetailsAction.ResetToFactory.Start -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val userWalletId = UserWalletIdBuilder.card(card).build()
// we must require a password regardless of biometric settings
val policy = tangemSdkManager.userCodeRequestPolicy
val doBeforeErase = {
val type = if (card.isAccessCodeSet) {
UserCodeType.AccessCode
} else if (card.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
}
type?.let {
tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type))
}
}
val doAfterErase = {
tangemSdkManager.setUserCodeRequestPolicy(policy)
}
doBeforeErase()
tangemSdkManager.resetToFactorySettings(card.cardId, true)
.flatMap {
userWalletsListManager.delete(listOfNotNull(userWalletId))
}
.flatMap {
tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId))
}
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}
.doOnFailure { error ->
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
}
}
.doOnResult {
doAfterErase()
}
}
}
else -> Unit
}
}
}
class ManageSecurityMiddleware {
@Suppress("ComplexMethod")
fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
when (action) {
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardSettingsState = detailsState.cardSettingsState
val cardId = cardSettingsState?.card?.cardId
val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
else -> return@launch
}
withContext(Dispatchers.Main) {
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption))
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
else -> Unit
}
}
}
}
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
when (tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {}
}
}
}
else -> Unit
}
}
}
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
@ -246,6 +97,7 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
@ -274,7 +126,7 @@ class DetailsMiddleware {
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
activityResultCaller.openSystemBiometrySettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
@ -395,7 +247,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
return CompletionResult.Success(Unit)
}
@ -429,63 +281,6 @@ class DetailsMiddleware {
}
}
class AccessCodeRecoveryMiddleware {
fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
when (action) {
is DetailsAction.AccessCodeRecovery.Open -> {
Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> {
scope.launch {
tangemSdkManager
.setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
),
)
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchOnMain(
DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
)
}
}
}
is DetailsAction.AccessCodeRecovery.SelectOption -> Unit
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit
}
}
}
private fun scanCard(state: DetailsState) = scope.launch {
store.inject(DaggerGraphState::scanCardProcessor)
.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
val isSameWallet = currentUserWalletId == scannedUserWalletId
if (isSameWallet) {
store.dispatchOnMain(
DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
@ -502,7 +297,7 @@ class DetailsMiddleware {
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
},
disclaimerWillShow = {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
onSuccess = { scanResponse ->
createUserWallet(scanResponse)
@ -546,7 +341,7 @@ class DetailsMiddleware {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
.doOnFailure { error ->
if (error is UserWalletsListError.WalletAlreadySaved) {
@ -554,7 +349,7 @@ class DetailsMiddleware {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
Timber.e(error, "Unable to create user wallet")
handleError(error, prevUseBiometricsForAccessCode)

View file

@ -1,21 +1,15 @@
package com.tangem.tap.features.details.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
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.domain.extensions.signedHashesCount
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import java.util.EnumSet
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
@ -29,16 +23,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state)
}
is DetailsAction.PrepareCardSettingsData -> {
handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
}
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
@ -47,7 +31,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
isScanningInProgress = true,
)
@ -61,19 +44,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.DismissError -> detailsState.copy(
error = null,
)
else -> detailsState
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
return DetailsState(
scanResponse = action.scanResponse,
// If the current screen is ResetToFactory, we must save the ResetToFactory's state
cardSettingsState = if (store.state.navigationState.backStack.lastOrNull() == AppScreen.ResetToFactory) {
state.detailsState.cardSettingsState
} else {
null
},
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking {
@ -96,139 +72,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
)
}
private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
val cardTypesResolver = scanResponse.cardTypesResolver
val card = scanResponse.card
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(cardTypesResolver),
scanResponse = scanResponse,
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
card = scanResponse.card,
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
resetButtonEnabled = false,
condition1Checked = false,
condition2Checked = false,
accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
AccessCodeRecoveryState(
enabledOnCard = enabled,
enabledSelection = enabled,
)
} else {
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
dialog = null,
)
return state.copy(cardSettingsState = cardSettingsState)
}
private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesResolver): ManageSecurityState {
val securityOption = when {
card.isAccessCodeSet -> {
SecurityOption.AccessCode
}
card.isPasscodeSet == true -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val allowedSecurityOptions = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
EnumSet.of(securityOption)
}
else ->
prepareAllowedSecurityOptions(cardTypesResolver = cardTypesResolver, currentSecurityOption = securityOption)
}
return ManageSecurityState(
currentOption = securityOption,
allowedOptions = allowedSecurityOptions,
selectedOption = securityOption,
)
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
val cardSettingsState = state.cardSettingsState
return when (action) {
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
warning1Checked && cardSettingsState.condition2Checked
} else {
warning1Checked
}
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = resetButtonEnabled,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
),
)
}
is DetailsAction.ResetToFactory.ShowDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog))
}
is DetailsAction.ResetToFactory.DismissDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null))
}
else -> state
}
}
private fun handleSecurityAction(action: DetailsAction.ManageSecurity, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SelectOption -> {
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
selectedOption = action.option,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
allowedOptions = state.scanResponse?.cardTypesResolver?.let {
prepareAllowedSecurityOptions(
cardTypesResolver = it,
currentSecurityOption = state.cardSettingsState.manageSecurityState.selectedOption,
)
} ?: EnumSet.of(SecurityOption.LongTap),
)
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
manageSecurityState = manageSecurityState,
),
)
}
else -> state
}
}
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -281,69 +124,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isHidingEnabled = action.hideBalance,
),
)
is DetailsAction.AppSettings.Prepare -> state.copy(
appSettingsState = action.state,
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}
private fun handleAccessCodeRecoveryAction(
action: DetailsAction.AccessCodeRecovery,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.AccessCodeRecovery.Open -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> state
is DetailsAction.AccessCodeRecovery.SelectOption -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledOnCard = action.enabled,
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
}
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}
@Suppress("MagicNumber")
private fun CardDTO.toCardInfo(cardTypesResolver: CardTypesResolver): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()
return CardInfo(
cardId = cardId,
issuer = issuer,
signedHashes = signedHashes,
isTwin = cardTypesResolver.isTangemTwins(),
hasBackup = backupStatus?.isActive == true,
)
}

View file

@ -3,15 +3,11 @@ package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
import org.rekotlin.StateType
import java.util.EnumSet
data class DetailsState(
val scanResponse: ScanResponse? = null,
val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
@ -19,52 +15,13 @@ data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int,
val isTwin: Boolean,
val hasBackup: Boolean,
)
/**
* @property enabledOnCard whether access code recovery is enabled on card
* @property enabledSelection current selected option in app (not saved on card yet)
*/
data class AccessCodeRecoveryState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
)
data class CardSettingsState(
val cardInfo: CardInfo,
val card: CardDTO,
val scanResponse: ScanResponse,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetButtonEnabled: Boolean,
val condition1Checked: Boolean,
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
val dialog: Dialog?,
) {
sealed interface Dialog {
data object StartResetDialog : Dialog
data object ContinueResetDialog : Dialog
data object InterruptedResetDialog : Dialog
data object CompletedResetDialog : Dialog
}
sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}
data class ManageSecurityState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
)
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,

View file

@ -1,19 +1,17 @@
package com.tangem.tap.features.details.redux.walletconnect
import androidx.core.os.bundleOf
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
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.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
@ -65,14 +63,7 @@ class WalletConnectMiddleware {
// TODO check
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
),
),
)
store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) }
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {

View file

@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents

View file

@ -6,8 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -22,12 +21,8 @@ internal class AppCurrencySelectorFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -40,7 +41,9 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
val listState = rememberLazyListState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
modifier = modifier
.imePadding()
.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TopBar(
@ -49,6 +52,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
state = state,
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
content = { paddingValues ->
val contentModifier = Modifier
.padding(paddingValues)
@ -62,7 +66,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
modifier = contentModifier,
listState = listState,
currencies = state.items,
selectedId = state.selectedId.orEmpty(),
selectedId = state.selectedId,
onCurrencyClick = state.onCurrencyClick,
)
}
@ -175,6 +179,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
@Composable
private fun LoadingList(modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(modifier = modifier) {
repeat(times = 10) {
Row(
@ -199,6 +204,7 @@ private fun LoadingList(modifier: Modifier = Modifier) {
)
}
}
Spacer(Modifier.height(bottomBarHeight))
}
}
@ -210,9 +216,11 @@ private fun CurrenciesList(
onCurrencyClick: (Currency) -> Unit,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
items(
items = currencies,

View file

@ -2,8 +2,9 @@ package com.tangem.tap.features.details.ui.appcurrency
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
@ -20,7 +21,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), AppCurrencySelectorIntents {
@ -34,7 +35,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
override fun onBackClick() {
reduxNavController.popBackStack()
router.pop()
}
override fun onSearchClick() {
@ -52,7 +53,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
analyticsEventHandler.send(
event = Settings.AppSettings.MainCurrencyChanged(currencyType = currency.name),
)
reduxNavController.popBackStack()
router.pop()
}
}
}

View file

@ -6,11 +6,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -35,8 +35,7 @@ internal class AppSettingsFragment : ComposeFragment() {
modifier = modifier,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
)
}

View file

@ -1,13 +1,17 @@
package com.tangem.tap.features.details.ui.appsettings
import android.content.res.Configuration
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -32,6 +36,7 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
},
titleRes = R.string.app_settings_title,
onBackClick = onBackClick,
addBottomInsets = false,
)
}
@ -44,7 +49,11 @@ private fun AppSettings(state: AppSettingsScreenState.Content) {
null -> Unit
}
LazyColumn {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
items(
items = state.items,
key = Item::id,

View file

@ -1,16 +1,20 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.features.details.redux.AppSetting
@ -26,14 +30,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class AppSettingsViewModel @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val balanceHidingRepository: BalanceHidingRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val detailsFeatureToggles: DetailsFeatureToggles,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
@ -50,6 +62,9 @@ internal class AppSettingsViewModel @Inject constructor(
init {
bootstrapAppCurrencyUpdates()
if (detailsFeatureToggles.isRedesignEnabled) {
bootstrapBiometricsUpdates()
}
subscribeToStoreChanges()
sendItemsAnalytics()
@ -128,7 +143,7 @@ internal class AppSettingsViewModel @Inject constructor(
}
private fun showAppCurrencySelector() {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppCurrencySelector))
store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) }
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
@ -214,6 +229,19 @@ internal class AppSettingsViewModel @Inject constructor(
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = viewModelScope.launch {
val state = AppSettingsState(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
)
store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state))
}
private fun subscribeToStoreChanges() {
store.subscribe(subscriber = this) { state ->
state.skipRepeats { oldState, newState ->

View file

@ -1,14 +1,12 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.viewModels
import com.tangem.core.navigation.NavigationAction
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -22,15 +20,8 @@ internal class CardSettingsFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
val state by viewModel.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(
modifier = modifier,
state = viewModel.screenState.value,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
CardSettingsScreen(modifier = modifier, state = state)
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -13,18 +16,14 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
internal fun CardSettingsScreen(
state: CardSettingsScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val needReadCard = state.cardDetails == null
SettingsScreensScaffold(
@ -37,7 +36,7 @@ internal fun CardSettingsScreen(
}
},
titleRes = R.string.card_settings_title,
onBackClick = onBackClick,
onBackClick = state.onBackClick,
)
}
@ -180,7 +179,7 @@ private fun CardSettings(state: CardSettingsScreenState) {
// region Preview
@Composable
private fun CardSettingsScreenStateSample() {
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}, onElementClick = {}), {})
CardSettingsScreen(state = CardSettingsScreenState(onBackClick = {}, onScanCardClick = {}, onElementClick = {}))
}
@Preview(showBackground = true, widthDp = 360)

View file

@ -4,18 +4,15 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.wallet.R
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
internal data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
val onBackClick: () -> Unit,
)
internal sealed class CardInfo(
@ -44,7 +41,7 @@ internal sealed class CardInfo(
clickable = clickable,
)
object ChangeAccessCode : CardInfo(
data object ChangeAccessCode : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_change_access_code),
subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer),
clickable = true,
@ -60,9 +57,9 @@ internal sealed class CardInfo(
clickable = true,
)
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
class ResetToFactorySettings(description: TextReference) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = cardInfo.toResetCardDescriptionText(),
subtitle = description,
clickable = true,
)
}

View file

@ -1,105 +1,132 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import arrow.core.Either
import androidx.lifecycle.viewModelScope
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.sdk.TangemSdkManager
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.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import org.rekotlin.StoreSubscriber
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) :
ViewModel(), DefaultLifecycleObserver, StoreSubscriber<DetailsState> {
private val scanCardProcessor: ScanCardProcessor,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(updateState(store.state.detailsState.cardSettingsState))
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.CardSettings.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
override fun onStart(owner: LifecycleOwner) {
when (val selectedWalletEither = getSelectedWalletSyncUseCase()) {
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
init {
cardSettingsInteractor.scannedScanResponse
.filterNotNull()
.onEach(::updateCardDetails)
.launchIn(viewModelScope)
}
private fun getInitialState() = CardSettingsScreenState(
cardDetails = null,
onElementClick = ::handleClickingItem,
onScanCardClick = ::scanCard,
onBackClick = ::onBackClick,
)
private fun scanCard() = viewModelScope.launch {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {
cardSettingsInteractor.initialize(scanResponse)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun updateCardDetails(scanResponse: ScanResponse) {
val card = scanResponse.card
val cardTypesResolver = scanResponse.cardTypesResolver
val cardId = cardTypesResolver.getCardId()
val currentSecurityOption = getCurrentSecurityOption(card)
val allowedSecurityOptions = getAllowedSecurityOptions(
card = card,
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver)
val cardDetails = buildList {
CardInfo.CardId(cardId).let(::add)
CardInfo.Issuer(card.issuer.name).let(::add)
if (!cardTypesResolver.isTangemTwins()) {
CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add)
}
CardInfo.SecurityMode(
currentSecurityOption,
clickable = allowedSecurityOptions.size > 1,
).let(::add)
if (card.backupStatus?.isActive == true && card.isAccessCodeSet) {
CardInfo.ChangeAccessCode.let(::add)
}
if (isAccessCodeRecoveryAllowed(cardTypesResolver)) {
CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add)
}
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(
isActiveBackupStatus = card.backupStatus?.isActive == true,
typesResolver = cardTypesResolver,
),
).let(::add)
}
is Either.Right -> Unit
}
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop(owner: LifecycleOwner) {
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
screenState.value = updateState(state.cardSettingsState)
}
private fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
accessCodeRecoveryState = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
)
} else {
val cardId = if (state.card.isTangemTwins) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
val cardDetails: MutableList<CardInfo> = mutableListOf(
CardInfo.CardId(cardId),
CardInfo.Issuer(state.cardInfo.issuer),
)
if (!state.card.isTangemTwins) {
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
}
cardDetails.add(
CardInfo.SecurityMode(
securityOption = state.manageSecurityState.currentOption,
clickable = state.manageSecurityState.allowedOptions.size > 1,
),
)
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(CardInfo.ChangeAccessCode)
}
if (state.accessCodeRecovery != null) {
cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
}
if (state.resetCardAllowed) {
cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
}
CardSettingsScreenState(
cardDetails = cardDetails,
accessCodeRecoveryState = state.accessCodeRecovery,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
},
)
screenState.update { state ->
state.copy(cardDetails = cardDetails)
}
}
@ -107,20 +134,77 @@ internal class CardSettingsViewModel @Inject constructor(
when (item) {
is CardInfo.ChangeAccessCode -> {
Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode))
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
changeAccessCode()
}
is CardInfo.ResetToFactorySettings -> {
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
store.dispatch(DetailsAction.ResetToFactory.Start)
resetWalletToFactorySettings()
}
is CardInfo.SecurityMode -> {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
store.dispatchNavigationAction {
push(route = AppRoute.DetailsSecurity(userWalletId))
}
}
is CardInfo.AccessCodeRecovery -> {
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) }
}
else -> {}
}
}
private fun resetWalletToFactorySettings() {
val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) {
"Impossible to reset card if ScanResponse is null"
}
if (scanResponse.cardTypesResolver.isTangemTwins()) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
cardSettingsInteractor.clear()
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
} else {
val card = scanResponse.card
store.dispatchNavigationAction {
push(
route = AppRoute.ResetToFactory(
userWalletId = userWalletId,
cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
),
)
}
}
}
private fun changeAccessCode() = viewModelScope.launch {
val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Scan response is null" }
when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
}
}
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
private fun onBackClick() {
cardSettingsInteractor.clear()
store.dispatchNavigationAction(AppRouter::pop)
}
}

View file

@ -1,54 +1,33 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.navigation.NavigationAction
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
class AccessCodeRecoveryFragment : ComposeFragment() {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
private val viewModel: AccessCodeRecoveryViewModel by viewModels()
@Inject
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value =
viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
}
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@ -57,7 +53,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = { state.onSaveChangesClick(state.enabledSelection) },
onClick = { state.onSaveChangesClick() },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
)
}

View file

@ -11,6 +11,6 @@ data class AccessCodeRecoveryScreenState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
val onSaveChangesClick: (Boolean) -> Unit,
val onSaveChangesClick: () -> Unit,
val onOptionClick: (Boolean) -> Unit,
)

View file

@ -1,29 +1,83 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
class AccessCodeRecoveryViewModel(val store: Store<AppState>) {
@HiltViewModel
internal class AccessCodeRecoveryViewModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
) : ViewModel() {
fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
// We shouldn't get to this screen here when this state is null
return if (state == null) {
AccessCodeRecoveryScreenState(
enabledOnCard = false,
enabledSelection = false,
isSaveChangesEnabled = false,
onSaveChangesClick = {},
onOptionClick = {},
)
} else {
AccessCodeRecoveryScreenState(
enabledOnCard = state.enabledOnCard,
enabledSelection = state.enabledSelection,
isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
?: error("Scan response is null")
val screenState = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): AccessCodeRecoveryScreenState {
val isEnabled = isAccessCodeRecoveryEnabled(
typeResolver = scannedScanResponse.cardTypesResolver,
card = scannedScanResponse.card,
)
return AccessCodeRecoveryScreenState(
enabledOnCard = isEnabled,
enabledSelection = isEnabled,
isSaveChangesEnabled = false,
onSaveChangesClick = ::saveChanges,
onOptionClick = ::selectOption,
)
}
private fun saveChanges() = viewModelScope.launch {
val isEnabled = screenState.value.enabledSelection
tangemSdkManager
.setAccessCodeRecoveryEnabled(scannedScanResponse.card.cardId, isEnabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(isEnabled),
),
)
cardSettingsInteractor.update { scanResponse ->
scanResponse.copy(
card = scanResponse.card.copy(
userSettings = scanResponse.card.userSettings?.copy(
isUserCodeRecoveryAllowed = isEnabled,
),
),
)
}
store.dispatchNavigationAction(AppRouter::pop)
}
}
private fun selectOption(isEnabled: Boolean) {
screenState.update {
it.copy(
enabledSelection = isEnabled,
isSaveChangesEnabled = isEnabled != it.enabledOnCard,
)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.features.details.ui.cardsettings.domain
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton
/**
* Interactor for sharing logic and data between all card settings screens
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class CardSettingsInteractor @Inject constructor() {
private val _scannedScanResponse = MutableStateFlow<ScanResponse?>(value = null)
val scannedScanResponse: StateFlow<ScanResponse?> = _scannedScanResponse
fun initialize(scanResponse: ScanResponse) {
_scannedScanResponse.value = scanResponse
}
fun update(transform: (ScanResponse) -> ScanResponse) {
_scannedScanResponse.update {
requireNotNull(it)
transform(it)
}
}
fun clear() {
_scannedScanResponse.value = null
}
}

View file

@ -13,8 +13,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.wallet.R
@Composable
@ -24,31 +24,42 @@ internal fun SettingsScreensScaffold(
modifier: Modifier = Modifier,
@StringRes titleRes: Int? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
addBottomInsets: Boolean = true,
fab: @Composable () -> Unit = {},
) {
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
SystemBarsEffect {
setSystemBarsColor(backgroundColor)
}
Scaffold(
scaffoldState = state,
topBar = {
EmptyTopBarWithNavigation(
modifier = Modifier.statusBarsPadding(),
onBackClick = onBackClick,
backgroundColor = backgroundColor,
)
},
modifier = modifier.systemBarsPadding(),
modifier = modifier,
contentWindowInsets = WindowInsetsZero,
backgroundColor = backgroundColor,
floatingActionButton = fab,
content = { paddings ->
floatingActionButton = {
Box(modifier = Modifier.navigationBarsPadding()) {
fab()
}
},
content = { paddingValues ->
Column(
modifier = Modifier
.padding(paddings)
.run {
if (addBottomInsets) {
navigationBarsPadding()
} else {
this
}
}
.padding(paddingValues)
.fillMaxSize(),
) {
if (titleRes != null) {
@ -81,9 +92,11 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
@Composable
internal fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color = TangemTheme.colors.background.primary,
) {
TopAppBar(
modifier = modifier,
title = { },
navigationIcon =
{

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boolean = typeResolver.isWallet2()
internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean =
if (typeResolver.isWallet2()) {
card.userSettings?.isUserCodeRecoveryAllowed ?: false
} else {
false
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(
isActiveBackupStatus: Boolean,
typesResolver: CardTypesResolver,
): TextReference {
return if (!isActiveBackupStatus || typesResolver.isTangemTwins()) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.redux.SecurityOption
import java.util.EnumSet
internal fun getCurrentSecurityOption(card: CardDTO): SecurityOption = when {
card.isAccessCodeSet -> SecurityOption.AccessCode
card.isPasscodeSet == true -> SecurityOption.PassCode
else -> SecurityOption.LongTap
}
internal fun getAllowedSecurityOptions(
card: CardDTO,
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption,
): EnumSet<SecurityOption> = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> EnumSet.of(SecurityOption.LongTap)
card.settings.isBackupAllowed -> EnumSet.of(currentSecurityOption)
else -> prepareAllowedSecurityOptions(
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}

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