Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-25 16:03:41 +04:00
parent 60ba581540
commit 88f7205293
15 changed files with 1 additions and 361 deletions

View file

@ -54,7 +54,6 @@ import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
@ -171,8 +170,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
@ -324,11 +321,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onStart() {
super.onStart()
TangemLogger.i("onStart")
dialogManager.onStart(this)
}
override fun onStop() {
dialogManager.onStop()
super.onStop()
TangemLogger.i("onStop")
}

View file

@ -1,52 +0,0 @@
package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class DialogManager : StoreSubscriber<GlobalState> {
var context: Context? = null
private var dialog: Dialog? = null
fun onStart(context: Context) {
this.context = context
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.globalState == newState.globalState
}.select { it.globalState }
}
}
fun onStop() {
this.context = null
store.unsubscribe(this)
}
@Suppress("LongMethod", "ComplexMethod")
override fun newState(state: GlobalState) {
if (state.dialog == null) {
dialog?.dismiss()
dialog = null
return
}
val context = context ?: return
if (dialog != null) return
dialog = when (state.dialog) {
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(
context = context,
source = state.dialog.source,
onTryAgain = state.dialog.onTryAgain,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
else -> null
}
dialog?.show()
}
}

View file

@ -1,14 +1,9 @@
package com.tangem.tap.common.extensions
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -45,26 +40,6 @@ suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
state.globalState.tapWalletManager.onWalletSelected(userWallet)
}
/**
* @param fatal used to indicate errors that should not normally occur
*/
fun Store<AppState>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<AppState>.dispatchDebugErrorNotification(error: TapError) {
inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error")))
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
dispatchOnMain(GlobalAction.ShowDialog(dialog))
}
fun Store<*>.dispatchDialogHide() {
dispatchOnMain(GlobalAction.HideDialog)
}
/**
* Dispatch action inside a coroutine with the Main dispatcher
*/
@ -76,14 +51,6 @@ suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
fun Store<AppState>.dispatchOpenUrl(url: String) {
inject(DaggerGraphState::urlOpener).openUrl(url)
}
fun Store<AppState>.dispatchShare(url: String) {
inject(DaggerGraphState::shareManager).shareText(url)
}
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
inject(DaggerGraphState::appRouter).action()
}

View file

@ -4,15 +4,10 @@ import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import org.rekotlin.Action
sealed class GlobalAction : Action {
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
object ScanFailsCounter {
data class ChooseBehavior(
val result: CompletionResult<ScanResponse>,

View file

@ -26,12 +26,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
is GlobalAction.ShowDialog -> {
globalState.copy(dialog = action.stateDialog)
}
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
else -> globalState
}
}

View file

@ -2,24 +2,16 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.TapWalletManager
import org.rekotlin.StateType
data class GlobalState(
@Deprecated("Use scan response from selected user wallet")
val scanResponse: ScanResponse? = null,
val onboardingState: OnboardingState = OnboardingState(),
val tapWalletManager: TapWalletManager = TapWalletManager(),
val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val isLastSignWithRing: Boolean = false,
) : StateType
typealias CryptoCurrencyName = String
data class OnboardingState(
val isOnboardingStarted: Boolean = false,
val shouldResetOnCreate: Boolean = false,
)
typealias CryptoCurrencyName = String

View file

@ -1,84 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
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.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
internal object ScanFailsDialog {
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
private const val RUSSIA_LOCALE = "ru"
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
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
}
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 = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link)
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.ScanningProblem)
}
}
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
store.dispatchDialogHide()
}
setView(customView)
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}
}

View file

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

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
object WalletActivationErrorDialog {
fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.onboarding_activation_error_title))
setMessage(context.getString(R.string.onboarding_activation_error_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val scanResponse = store.state.globalState.scanResponse
?: error("ScanResponse must be not null")
val cardInfo = store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}

View file

@ -2,8 +2,6 @@ package com.tangem.tap.proxy
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -32,8 +30,4 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder {
override suspend fun onUserWalletSelected(userWallet: UserWallet) {
mainStore?.onUserWalletSelected(userWallet)
}
override fun dispatchDialogShow(dialog: StateDialog) {
mainStore?.dispatchDialogShow(dialog)
}
}

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_primary"
android:orientation="vertical"
android:paddingVertical="16dp"
tools:layout_gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:text="@string/common_warning"
android:textAlignment="center"
android:textColor="@color/text_primary_1"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:layout_marginTop="24dp"
android:text="@string/alert_troubleshooting_scan_card_title"
android:textAlignment="center"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/try_again_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:layout_marginTop="24dp"
android:text="@string/alert_button_try_again"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/menu_accent_color"
android:textSize="16sp" />
<TextView
android:id="@+id/how_to_scan_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:layout_marginTop="24dp"
android:text="@string/alert_button_how_to_scan"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/menu_accent_color"
android:textSize="16sp" />
<TextView
android:id="@+id/request_support_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:layout_marginTop="24dp"
android:text="@string/alert_button_request_support"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/menu_accent_color"
android:textSize="16sp" />
<TextView
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="38dp"
android:layout_marginTop="24dp"
android:text="@string/common_cancel"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/warning_critical"
android:textSize="16sp" />
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -10,6 +10,4 @@ interface ReduxStateHolder {
suspend fun dispatchWithMain(action: Action)
suspend fun onUserWalletSelected(userWallet: UserWallet)
fun dispatchDialogShow(dialog: StateDialog)
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.redux
interface StateDialog {
data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog
enum class ScanFailsSource {
MAIN, SIGN_IN, SETTINGS, INTRO
}
}

View file

@ -17,8 +17,6 @@ import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
@ -35,7 +33,6 @@ import javax.inject.Inject
internal class DefaultWalletRouter @Inject constructor(
private val router: AppRouter,
private val urlOpener: UrlOpener,
private val reduxStateHolder: ReduxStateHolder,
private val designFeatureToggles: DesignFeatureToggles,
) : InnerWalletRouter {
@ -117,10 +114,6 @@ internal class DefaultWalletRouter @Inject constructor(
return router.stack.lastOrNull() is AppRoute.Wallet
}
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}
override fun openNFT(userWallet: UserWallet) {
router.push(
AppRoute.NFT(

View file

@ -66,9 +66,6 @@ internal interface InnerWalletRouter {
/** Is wallet last screen */
fun isWalletLastScreen(): Boolean
/** Open scan failed dialog */
fun openScanFailedDialog(onTryAgain: () -> Unit)
/** Open NFT collections screen */
fun openNFT(userWallet: UserWallet)