Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-02 11:04:24 +03:00
commit d314c8733a
21 changed files with 57 additions and 40 deletions

View file

@ -108,11 +108,11 @@ internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
get() = Job() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
val scope = CoroutineScope(coroutineContext)
private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
get() = SupervisorJob() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
@Suppress("LargeClass")

View file

@ -623,7 +623,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
if (action.unfinishedBackupScanResponse != null) {
// onboarding V2
store.dispatchNavigationAction {
push(AppRoute.Onboarding(scanResponse = action.unfinishedBackupScanResponse))
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
} else {
store.dispatch(

View file

@ -16,7 +16,7 @@ import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding
class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
var binding: LayoutBackupAccessCodeBinding? = null
private var binding: LayoutBackupAccessCodeBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -33,7 +33,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
}
}
fun showInfoScreen() = with(binding!!) {
fun showInfoScreen() = binding?.run {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
layoutBackupAccessCodeInfo.root.show()
@ -43,7 +43,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
}
}
fun showEnterAccessCode() = with(binding!!) {
fun showEnterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
@ -56,7 +56,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
}
}
fun showReenterAccessCode() = with(binding!!) {
fun showReenterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
@ -69,7 +69,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
}
}
fun showError(error: AccessCodeError?) = with(binding!!.layoutBackupAccessCodeSubmit) {
fun showError(error: AccessCodeError?) = binding?.layoutBackupAccessCodeSubmit?.run {
when (error) {
AccessCodeError.CodeTooShort ->
tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short)

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.finisher.AppFinisher
@ -14,7 +15,6 @@ import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -23,6 +23,7 @@ import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// FIXME: Remove redux: [REDACTED_JIRA]
@ModelScoped
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appFinisher: AppFinisher,
@ -40,8 +41,6 @@ internal class WelcomeModel @Inject constructor(
val state: MutableStateFlow<WelcomeScreenState> = MutableStateFlow(initialState)
init {
store.dispatch(WelcomeAction.SetCoroutineScope(mainScope))
subscribeToStoreChanges()
initGlobalState()
@ -87,7 +86,6 @@ internal class WelcomeModel @Inject constructor(
}
override fun onDestroy() {
store.dispatch(WelcomeAction.ClearCoroutineScope)
store.unsubscribe(subscriber = this)
super.onDestroy()

View file

@ -2,15 +2,10 @@ package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemError
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
internal sealed interface WelcomeAction : Action {
data class SetCoroutineScope(val scope: CoroutineScope) : WelcomeAction
object ClearCoroutineScope : WelcomeAction
data class ProceedWithBiometrics(val afterUnlockIntent: Intent? = null) : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction

View file

@ -21,6 +21,7 @@ import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -44,7 +45,7 @@ internal class WelcomeMiddleware {
}
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
state.scope?.launch {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(

View file

@ -14,8 +14,6 @@ internal object WelcomeReducer {
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
return when (action) {
is WelcomeAction.SetCoroutineScope -> state.copy(scope = action.scope)
is WelcomeAction.ClearCoroutineScope -> state.copy(scope = null)
is WelcomeAction.ProceedWithIntent -> state.copy(intent = action.intent)
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)

View file

@ -2,11 +2,9 @@ package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemError
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.StateType
data class WelcomeState(
val scope: CoroutineScope? = null,
val isUnlockWithBiometricsInProgress: Boolean = false,
val isUnlockWithCardInProgress: Boolean = false,
val intent: Intent? = null,

View file

@ -216,6 +216,8 @@ internal class ChildFactory @Inject constructor(
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
AppRoute.Onboarding.Mode.ContinueFinalize ->
OnboardingEntryComponent.Mode.ContinueFinalize
},
),
componentFactory = onboardingEntryComponentFactory,
@ -729,6 +731,8 @@ internal class ChildFactory @Inject constructor(
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
AppRoute.Onboarding.Mode.ContinueFinalize ->
OnboardingEntryComponent.Mode.ContinueFinalize
},
),
componentFactory = onboardingEntryComponentFactory,

View file

@ -268,6 +268,7 @@ sealed class AppRoute(val path: String) : Route {
AddBackupWallet1, // continue backup process for existing wallet 1
WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins
RecreateWalletTwin, // reset twins
ContinueFinalize, // continue finalize process (unfinished backup dialog)
}
}

View file

@ -7,12 +7,14 @@ 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 TxHistoryItemsStoreModule {
@Provides
@Singleton
fun provideTxHistoryItemsStore(): TxHistoryItemsStore {
return DefaultTxHistoryItemsStore(
dataStore = RuntimeDataStore(),

View file

@ -2,7 +2,6 @@ package com.tangem.core.ui.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@ -16,7 +15,7 @@ import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun requestPushPermission(
pushPermission: String?,
isClicked: MutableState<Boolean>,
isClicked: Boolean,
onAllow: () -> Unit,
onDeny: () -> Unit,
): () -> Unit {
@ -26,7 +25,7 @@ fun requestPushPermission(
// Check if user granted permission and close bottom sheet
LaunchedEffect(key1 = permissionState?.status, isClicked) {
if (!isClicked.value) return@LaunchedEffect
if (!isClicked) return@LaunchedEffect
if (permissionState?.status?.isGranted == true) {
onAllow()
} else {

View file

@ -16,6 +16,7 @@ interface OnboardingEntryComponent : ComposableContentComponent {
AddBackupWallet1,
WelcomeOnlyTwin,
RecreateWalletTwin,
ContinueFinalize,
}
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -81,6 +81,7 @@ internal class OnboardingEntryModel @Inject constructor(
val multiWalletNavigationMode = when (params.mode) {
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
else -> error("Incorrect onboarding type")
}

View file

@ -20,6 +20,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
enum class Mode {
Onboarding,
AddBackup,
ContinueFinalize,
}
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>

View file

@ -39,9 +39,8 @@ fun Wallet1ChooseOption(
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.weight(1f)
.padding(top = 32.dp, bottom = 16.dp),
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val pagerState = rememberPagerState { CarouselItems.size }
@ -49,6 +48,8 @@ fun Wallet1ChooseOption(
HorizontalPager(pagerState) { selectedIndex ->
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {

View file

@ -226,7 +226,9 @@ internal class MultiWalletFinalizeModel @Inject constructor(
val userWalletCreated = createUserWallet(scanResponse)
val userWallet = when (params.parentParams.mode) {
OnboardingMultiWalletComponent.Mode.Onboarding -> {
OnboardingMultiWalletComponent.Mode.Onboarding,
OnboardingMultiWalletComponent.Mode.ContinueFinalize,
-> {
userWalletsListManager.save(
userWallet = userWalletCreated.copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),

View file

@ -46,7 +46,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
currentStep = getInitialStep(),
currentScanResponse = params.scanResponse,
accessCode = null,
isThreeCards = true,
isThreeCards = isThreeCardsInit(),
resultUserWallet = null,
startFromFinalize = getInitialStartFromFinalize(),
),
@ -102,6 +102,8 @@ internal class OnboardingMultiWalletModel @Inject constructor(
val card = scanResponse.card
return when {
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize ->
OnboardingMultiWalletState.Step.Finalize
// Add backup button
// Wallet1 without backup and userwallet's scanResponse doesn't contain primary card.
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup &&
@ -134,6 +136,18 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
}
private fun isThreeCardsInit(): Boolean {
val backupService = backupServiceHolder.backupService.get() ?: return true
return when (backupService.currentState) {
BackupService.State.FinalizingPrimaryCard,
is BackupService.State.FinalizingBackupCard,
-> {
backupService.addedBackupCardsCount > 1
}
else -> true
}
}
private fun initScreenTitle() {
val title = screenTitleByStep(getInitialStep())
params.titleProvider.changeTitle(title)

View file

@ -17,7 +17,7 @@ import javax.inject.Inject
@Stable
@ModelScoped
class PushNotificationsModel @Inject constructor(
internal class PushNotificationsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,

View file

@ -1,9 +1,7 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.showcase.Showcase
import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel
@ -21,7 +19,7 @@ internal fun PushNotificationsScreen(
onAllowPermission: () -> Unit,
onDenyPermission: () -> Unit,
) {
val isClicked = remember { mutableStateOf(false) }
var isClicked by remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
isClicked = isClicked,
onAllow = onAllowPermission,
@ -45,7 +43,7 @@ internal fun PushNotificationsScreen(
primaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_allow),
onClick = {
isClicked.value = true
isClicked = true
onRequest()
requestPushPermission()
},

View file

@ -6,9 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
@ -36,7 +34,7 @@ internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) {
@Composable
private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) {
val isClicked = remember { mutableStateOf(false) }
var isClicked by remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
pushPermission = getPushPermissionOrNull(),
isClicked = isClicked,
@ -86,7 +84,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC
PrimaryButton(
text = stringResourceSafe(R.string.common_allow),
onClick = {
isClicked.value = true
isClicked = true
content.onRequest()
requestPushPermission()
},