Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-07 11:56:45 +03:00
parent 342b6a2f50
commit ec40334d8a
26 changed files with 527 additions and 50 deletions

View file

@ -52,6 +52,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.api.navigation.SendRouter
@ -59,6 +60,7 @@ import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.operations.backup.BackupService import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.init import com.tangem.sdk.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.ActivityResultCallbackHolder
@ -185,6 +187,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject @Inject
lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
@Inject
lateinit var backupServiceHolder: BackupServiceHolder
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
internal val viewModel: MainViewModel by viewModels() internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode> private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -294,7 +302,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
cardSdkOwner.register(activity = this) cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
} else {
backupService = BackupService.init(cardSdkConfigRepository.sdk, this) backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
}
lockUserWalletsTimer = LockUserWalletsTimer( lockUserWalletsTimer = LockUserWalletsTimer(
owner = this, owner = this,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
@ -550,6 +565,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
} }
} }
// TODO add onboarding v2 handling
store.dispatch(BackupAction.CheckForUnfinishedBackup) store.dispatch(BackupAction.CheckForUnfinishedBackup)
} }

View file

@ -0,0 +1,17 @@
package com.tangem.tap.di
import com.tangem.sdk.api.BackupServiceHolder
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 TangemSdkModule {
@Provides
@Singleton
fun provideBackupServiceHolder(): BackupServiceHolder = BackupServiceHolder()
}

View file

@ -67,7 +67,10 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute { fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) { if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) {
return AppRoute.Onboarding(scanResponse) return AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = false,
)
} }
return when (val type = scanResponse.productType) { return when (val type = scanResponse.productType) {

View file

@ -218,7 +218,10 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Onboarding -> { is AppRoute.Onboarding -> {
route.asComponentChild( route.asComponentChild(
contextProvider = contextProvider(route, contextFactory), contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(route.scanResponse), params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
),
componentFactory = onboardingEntryComponentFactory, componentFactory = onboardingEntryComponentFactory,
) )
} }

View file

@ -300,5 +300,8 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}") ) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
// Onboarding V2 // Onboarding V2
data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2") data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean,
) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}")
} }

View file

@ -8,6 +8,7 @@ interface OnboardingEntryComponent : ComposableContentComponent {
data class Params( data class Params(
val scanResponse: ScanResponse, val scanResponse: ScanResponse,
val startBackupFlow: Boolean,
) )
interface Factory : ComponentFactory<Params, OnboardingEntryComponent> interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.core) implementation(projects.domain.core)
implementation(projects.domain.card)
/** Tangem libraries */ /** Tangem libraries */
implementation(projects.libs.tangemSdkApi) implementation(projects.libs.tangemSdkApi)
@ -54,6 +55,7 @@ dependencies {
implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization) implementation(deps.kotlin.serialization)
implementation(deps.timber) implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */ /** DI */
implementation(deps.hilt.android) implementation(deps.hilt.android)

View file

@ -40,12 +40,12 @@ internal class OnboardingEntryModel @Inject constructor(
return when (scanResponse.productType) { return when (scanResponse.productType) {
ProductType.Note -> TODO() ProductType.Note -> TODO()
ProductType.Twins -> TODO() ProductType.Twins -> TODO()
ProductType.Wallet -> OnboardingRoute.Wallet12( ProductType.Wallet -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse, scanResponse = scanResponse,
withSeedPhraseFlow = false, withSeedPhraseFlow = false,
titleProvider = titleProvider, titleProvider = titleProvider,
) )
ProductType.Wallet2 -> OnboardingRoute.Wallet12( ProductType.Wallet2 -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse, scanResponse = scanResponse,
withSeedPhraseFlow = true, withSeedPhraseFlow = true,
titleProvider = titleProvider, titleProvider = titleProvider,

View file

@ -5,12 +5,12 @@ import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletCo
import javax.inject.Inject import javax.inject.Inject
internal class OnboardingChildFactory @Inject constructor( internal class OnboardingChildFactory @Inject constructor(
private val wallet12ComponentFactory: OnboardingMultiWalletComponent.Factory, private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory,
) { ) {
fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any { fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any {
return when (route) { return when (route) {
is OnboardingRoute.Wallet12 -> wallet12ComponentFactory.create( is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create(
context = childContext, context = childContext,
params = OnboardingMultiWalletComponent.Params( params = OnboardingMultiWalletComponent.Params(
scanResponse = route.scanResponse, scanResponse = route.scanResponse,

View file

@ -8,7 +8,7 @@ sealed class OnboardingRoute : Route {
data object None : OnboardingRoute() data object None : OnboardingRoute()
data class Wallet12( data class MultiWallet(
val titleProvider: TitleProvider, val titleProvider: TitleProvider,
val scanResponse: ScanResponse, val scanResponse: ScanResponse,
val withSeedPhraseFlow: Boolean, val withSeedPhraseFlow: Boolean,

View file

@ -31,7 +31,7 @@ internal inline fun OnboardingEntry(
animation = stackAnimation(slide()), animation = stackAnimation(slide()),
) { ) {
when (it.configuration) { when (it.configuration) {
is OnboardingRoute.Wallet12 -> { is OnboardingRoute.MultiWallet -> {
(it.instance as OnboardingMultiWalletComponent).Content( (it.instance as OnboardingMultiWalletComponent).Content(
modifier = modifier, modifier = modifier,
) )

View file

@ -32,12 +32,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
) : OnboardingMultiWalletComponent, AppComponentContext by context { ) : OnboardingMultiWalletComponent, AppComponentContext by context {
private val model: OnboardingMultiWalletModel = getOrCreateModel(params) private val model: OnboardingMultiWalletModel = getOrCreateModel(params)
private val stackNavigation = StackNavigation<OnboardingMultiWalletState.Step>() private val stackNavigation = StackNavigation<OnboardingMultiWalletState.Step>()
override val innerNavigation: InnerNavigation = object : InnerNavigation { override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state = MutableStateFlow( override val state = MutableStateFlow(
Wallet12InnerNavigationState(1, 5), MultiWalletInnerNavigationState(1, 5),
) )
override fun pop(onComplete: (Boolean) -> Unit) { override fun pop(onComplete: (Boolean) -> Unit) {
@ -83,8 +82,10 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private fun onStepDone() { private fun onStepDone() {
when (model.state.value.currentStep) { when (model.state.value.currentStep) {
OnboardingMultiWalletState.Step.CreateWallet -> OnboardingMultiWalletState.Step.CreateWallet -> {
stackNavigation.push(OnboardingMultiWalletState.Step.AddBackupDevice) stackNavigation.push(OnboardingMultiWalletState.Step.AddBackupDevice)
// innerNavigation.state.value TODO
}
OnboardingMultiWalletState.Step.AddBackupDevice -> TODO() OnboardingMultiWalletState.Step.AddBackupDevice -> TODO()
OnboardingMultiWalletState.Step.FinishBackup -> TODO() OnboardingMultiWalletState.Step.FinishBackup -> TODO()
OnboardingMultiWalletState.Step.Done -> TODO() OnboardingMultiWalletState.Step.Done -> TODO()
@ -112,7 +113,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
} }
} }
data class Wallet12InnerNavigationState( data class MultiWalletInnerNavigationState(
override val stackSize: Int, override val stackSize: Int,
override val stackMaxSize: Int?, override val stackMaxSize: Int?,
) : InnerNavigationState ) : InnerNavigationState

View file

@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.MultiWalletBackup import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.MultiWalletBackup
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
class MultiWalletBackupComponent( class MultiWalletBackupComponent(
@ -26,6 +27,10 @@ class MultiWalletBackupComponent(
params.parentParams.titleProvider.changeTitle( params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.onboarding_navbar_title_creating_backup), text = resourceReference(R.string.onboarding_navbar_title_creating_backup),
) )
componentScope.launch {
model.onDoneFlow.collect { onDone() }
}
} }
@Composable @Composable

View file

@ -0,0 +1,48 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
import android.graphics.Bitmap
import com.tangem.common.card.Card
// TODO maybe delete some fields in the future
data class BackupState(
val primaryCardId: String? = null,
val primaryCardBatchId: String? = null,
val backupCardsNumber: Int = 0,
val backupCards: List<Card> = emptyList(),
val backupCardIds: List<CardId> = emptyList(),
val backupCardBatchIds: List<CardId> = emptyList(),
@Transient
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep? = null,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
val startedSource: BackupStartedSource = BackupStartedSource.Onboarding,
val hasRing: Boolean = false,
)
enum class BackupStartedSource {
Onboarding, CreateBackup
}
enum class AccessCodeError {
CodeTooShort, CodesDoNotMatch
}
typealias CardId = String
sealed class BackupStep {
data object InitBackup : BackupStep()
data object ScanOriginCard : BackupStep()
data object AddBackupCards : BackupStep()
data object SetAccessCode : BackupStep()
data object EnterAccessCode : BackupStep()
data object ReenterAccessCode : BackupStep()
data object WritePrimaryCard : BackupStep()
data class WriteBackupCard(val cardNumber: Int) : BackupStep()
data object Finished : BackupStep()
}

View file

@ -1,31 +1,201 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.scan.ProductType
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.resetBackupCardDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ComponentScoped @ComponentScoped
class MultiWalletBackupModel @Inject constructor( class MultiWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val tangemSdkManager: TangemSdkManager,
) : Model() { ) : Model() {
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<MultiWalletChildParams>() private val params = paramsContainer.require<MultiWalletChildParams>()
private val scanResponse
get() = params.multiWalletState.value.currentScanResponse
private val backupService
get() = backupServiceHolder.backupService.get()!!
private val state = MutableStateFlow(BackupState())
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
MultiWalletBackupUM( when (scanResponse.productType) {
finalizeButtonEnabled = false, ProductType.Wallet -> getWallet1State()
ProductType.Wallet2,
ProductType.Ring,
-> MultiWalletBackupUM.Wallet2(
isRing = scanResponse.productType == ProductType.Ring,
backupAdded = false, // backupService.hasIncompletedBackup,
onAddBackupClick = {}, onAddBackupClick = {},
onFinalizeButtonClick = {}, onFinalizeButtonClick = {},
), )
else -> error("Type: ${scanResponse.productType.name} is not supported!")
},
) )
val uiState: StateFlow<MultiWalletBackupUM> = _uiState val uiState: StateFlow<MultiWalletBackupUM> = _uiState
val onDoneFlow = MutableSharedFlow<Unit>()
private fun getWallet1State(): MultiWalletBackupUM.Wallet1 {
return when (backupService.currentState) {
BackupService.State.Preparing -> {
MultiWalletBackupUM.Wallet1(
title = resourceReference(R.string.onboarding_title_no_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards),
showFinalizeButton = false,
addBackupButtonEnabled = true,
addBackupButtonLoading = false,
artworkNumberOfBackupCards = 0,
onAddBackupClick = ::startBackupWallet1,
onFinalizeButtonClick = {},
onSkipButtonClick = { onDoneFlow.tryEmit(Unit) },
)
}
is BackupService.State.FinalizingBackupCard -> TODO()
BackupService.State.FinalizingPrimaryCard -> TODO()
BackupService.State.Finished -> TODO()
}
}
private fun startBackupWallet1() {
backupService.discardSavedBackup()
val primaryCard = scanResponse.primaryCard
if (primaryCard != null) {
backupService.setPrimaryCard(primaryCard)
addBackupCardWithService()
} else {
// TODO wallet 2 ??? or from main?
// BackupAction.StartAddingPrimaryCard
}
}
private fun setNumberOfBackupCardsWallet1(number: Int) {
// set state for Wallet1 for adding backup cards and disable button if there is more than 2 backup cards
_uiState.update { st ->
if (st !is MultiWalletBackupUM.Wallet1) return@update st
when (number) {
0 -> {
st.copy(
title = resourceReference(R.string.onboarding_title_no_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards),
artworkNumberOfBackupCards = 0,
addBackupButtonEnabled = true,
)
}
1 -> {
st.copy(
title = resourceReference(R.string.onboarding_title_one_backup_card),
bodyText = resourceReference(R.string.onboarding_subtitle_one_backup_card),
artworkNumberOfBackupCards = 1,
addBackupButtonEnabled = true,
showFinalizeButton = true,
)
}
2 -> {
st.copy(
title = resourceReference(R.string.onboarding_title_two_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_two_backup_cards),
artworkNumberOfBackupCards = 2,
addBackupButtonEnabled = false,
)
}
else -> st.copy(addBackupButtonEnabled = false)
}
}
}
private fun addBackupCardWithService() {
updateWallet1State { st ->
st.copy(addBackupButtonLoading = true)
}
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
cardSdkConfigRepository.sdk.config.filter.cardIdFilter = null
updateWallet1State { st ->
st.copy(addBackupButtonLoading = false)
}
when (result) {
is CompletionResult.Success -> {
// TODO change artwork
state.update {
it.copy(
backupCards = it.backupCards + result.data,
backupCardsNumber = it.backupCardsNumber + 1,
)
}
setNumberOfBackupCardsWallet1(state.value.backupCardsNumber)
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.BackupFailedNotEmptyWallets -> {
updateWallet1State { st ->
st.copy(
dialog = resetBackupCardDialog(
onReset = { resetBackupCard(cardId = error.cardId) },
onDismiss = { updateWallet1State { it.copy(dialog = null) } },
),
)
}
}
is TangemSdkError.IssuerSignatureLoadingFailed -> {
updateWallet1State { st ->
st.copy(
dialog = backupCardAttestationFailedDialog(
onDismiss = { updateWallet1State { it.copy(dialog = null) } },
),
)
}
}
else -> FirebaseCrashlytics.getInstance().recordException(result.error)
}
}
}
}
}
private fun resetBackupCard(cardId: String) {
modelScope.launch {
tangemSdkManager.resetToFactorySettings(
cardId = cardId,
allowsRequestAccessCodeFromRepository = false,
)
}
}
private fun updateWallet1State(block: (MultiWalletBackupUM.Wallet1) -> MultiWalletBackupUM.Wallet1) {
_uiState.update { state ->
if (state !is MultiWalletBackupUM.Wallet1) return@update state
block(state)
}
}
} }

View file

@ -0,0 +1,27 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUMDialog
internal fun resetBackupCardDialog(onReset: () -> Unit, onDismiss: () -> Unit) = MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_attention),
bodyText = resourceReference(R.string.onboarding_linking_error_card_with_wallets),
confirmText = resourceReference(R.string.common_cancel),
cancelText = resourceReference(R.string.card_settings_action_sheet_reset),
warningCancelColor = true,
onDismiss = onDismiss,
onConfirm = onDismiss,
onCancel = { onReset(); onDismiss() },
)
internal fun backupCardAttestationFailedDialog(onDismiss: () -> Unit) = MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_error),
bodyText = resourceReference(R.string.issuer_signature_loading_failed),
confirmText = resourceReference(R.string.common_ok),
cancelText = null,
warningCancelColor = true,
onDismiss = onDismiss,
onConfirm = onDismiss,
onCancel = null,
)

View file

@ -4,15 +4,19 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM
@ -38,11 +42,90 @@ fun MultiWalletBackup(state: MultiWalletBackupUM, modifier: Modifier = Modifier)
) )
} }
when (state) {
is MultiWalletBackupUM.Wallet1 -> Wallet1Content(state)
is MultiWalletBackupUM.Wallet2 -> TODO()
}
}
val dialog = state.dialog
if (dialog != null) {
BasicDialog(
title = dialog.title.resolveReference(),
message = dialog.bodyText.resolveReference(),
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
onClick = dialog.onConfirm,
),
dismissButton = if (dialog.cancelText != null && dialog.onCancel != null) {
DialogButtonUM(
title = dialog.cancelText.resolveReference(),
warning = dialog.warningCancelColor,
onClick = dialog.onCancel,
)
} else {
null
},
onDismissDialog = dialog.onDismiss,
)
}
}
@Composable
private fun ColumnScope.Wallet1Content(state: MultiWalletBackupUM.Wallet1) {
Column(
Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp, bottom = 74.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// artwork
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = state.bodyText.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
if (state.showFinalizeButton) {
SecondaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
enabled = state.addBackupButtonEnabled,
showProgress = state.addBackupButtonLoading,
text = stringResource(R.string.onboarding_button_add_backup_card),
onClick = state.onAddBackupClick,
)
PrimaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResource(R.string.onboarding_button_finalize_backup),
onClick = state.onFinalizeButtonClick,
)
} else {
PrimaryButtonIconEnd( PrimaryButtonIconEnd(
modifier = Modifier modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp) .padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth(), .fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24, iconResId = R.drawable.ic_tangem_24,
enabled = state.addBackupButtonEnabled,
showProgress = state.addBackupButtonLoading,
text = stringResource(R.string.onboarding_button_add_backup_card), text = stringResource(R.string.onboarding_button_add_backup_card),
onClick = state.onAddBackupClick, onClick = state.onAddBackupClick,
) )
@ -51,20 +134,33 @@ fun MultiWalletBackup(state: MultiWalletBackupUM, modifier: Modifier = Modifier)
modifier = Modifier modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
enabled = state.finalizeButtonEnabled, text = stringResource(R.string.onboarding_button_skip_backup),
text = stringResource(R.string.onboarding_button_finalize_backup), onClick = state.onSkipButtonClick,
onClick = state.onFinalizeButtonClick,
) )
} }
} }
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun PreviewWallet1() {
TangemThemePreview { TangemThemePreview {
MultiWalletBackup( MultiWalletBackup(
state = MultiWalletBackupUM( state = MultiWalletBackupUM.Wallet1(
finalizeButtonEnabled = true, title = stringReference("Title"),
bodyText = stringReference("Body body body"),
),
)
}
}
@Preview
@Composable
private fun PreviewWallet2() {
TangemThemePreview {
MultiWalletBackup(
state = MultiWalletBackupUM.Wallet2(
isRing = false,
backupAdded = true,
onAddBackupClick = {}, onAddBackupClick = {},
onFinalizeButtonClick = {}, onFinalizeButtonClick = {},
), ),

View file

@ -0,0 +1,21 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun Wallet1Artwork(modifier: Modifier = Modifier) {
Box(modifier) {
}
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
Wallet1Artwork()
}
}

View file

@ -1,7 +1,45 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state
data class MultiWalletBackupUM( import androidx.annotation.IntRange
val finalizeButtonEnabled: Boolean, import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
@Immutable
sealed class MultiWalletBackupUM {
abstract val dialog: MultiWalletBackupUMDialog?
data class Wallet1(
val title: TextReference = stringReference(""),
val bodyText: TextReference = stringReference(""),
val addBackupButtonEnabled: Boolean = true,
val addBackupButtonLoading: Boolean = false,
val showFinalizeButton: Boolean = false,
val oneBackupAdded: Boolean = false,
@IntRange(0, 2) val artworkNumberOfBackupCards: Int = 0, // highlights cards on 0, 1, 2
val onAddBackupClick: () -> Unit = {},
val onFinalizeButtonClick: () -> Unit = {},
val onSkipButtonClick: () -> Unit = {},
override val dialog: MultiWalletBackupUMDialog? = null,
) : MultiWalletBackupUM()
data class Wallet2(
val isRing: Boolean,
val backupAdded: Boolean,
val onAddBackupClick: () -> Unit, val onAddBackupClick: () -> Unit,
val onFinalizeButtonClick: () -> Unit, val onFinalizeButtonClick: () -> Unit,
override val dialog: MultiWalletBackupUMDialog? = null,
) : MultiWalletBackupUM()
}
data class MultiWalletBackupUMDialog(
val title: TextReference,
val bodyText: TextReference,
val confirmText: TextReference,
val cancelText: TextReference?,
val warningCancelColor: Boolean = false,
val onConfirm: () -> Unit,
val onCancel: (() -> Unit)?,
val onDismiss: () -> Unit,
) )

View file

@ -4,13 +4,20 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
internal fun resetCardDialog(onConfirm: () -> Unit, onDismiss: () -> Unit, onDismissButtonClick: () -> Unit) = internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) =
MultiWalletCreateWalletUM.Dialog( MultiWalletCreateWalletUM.Dialog(
title = resourceReference(R.string.onboarding_activation_error_title), title = resourceReference(R.string.onboarding_activation_error_title),
description = resourceReference(R.string.onboarding_activation_error_message), description = resourceReference(R.string.onboarding_activation_error_message),
confirmButtonText = resourceReference(R.string.common_ok), confirmButtonText = resourceReference(R.string.common_support),
dismissButtonText = resourceReference(R.string.common_support), dismissButtonText = resourceReference(R.string.common_ok),
onDismiss = onDismiss, dismissWarningColor = true,
onConfirm = onConfirm, onDismiss = dismiss,
onDismissButtonClick = onDismissButtonClick, onConfirm = {
onConfirm()
dismiss()
},
onDismissButtonClick = {
onDismissButtonClick()
dismiss()
},
) )

View file

@ -5,6 +5,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
@ -26,6 +27,7 @@ class MultiWalletCreateWalletModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager, private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase, private val getCardInfoUseCase: GetCardInfoUseCase,
private val cardRepository: CardRepository,
) : Model() { ) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>() private val params = paramsContainer.require<MultiWalletChildParams>()
@ -63,6 +65,7 @@ class MultiWalletCreateWalletModel @Inject constructor(
) )
} }
cardRepository.startCardActivation(cardId = result.data.card.cardId)
onDone.emit(Unit) onDone.emit(Unit)
// TODO // TODO
// Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) // Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
@ -82,14 +85,9 @@ class MultiWalletCreateWalletModel @Inject constructor(
_uiState.update { state -> _uiState.update { state ->
state.copy( state.copy(
dialog = resetCardDialog( dialog = resetCardDialog(
onConfirm = { onConfirm = ::navigateToSupportScreen,
_uiState.update { it.copy(dialog = null) } dismiss = { _uiState.update { it.copy(dialog = null) } },
resetCard() onDismissButtonClick = ::resetCard,
},
onDismiss = {
_uiState.update { it.copy(dialog = null) }
},
onDismissButtonClick = ::navigateToSupportScreen,
), ),
) )
} }

View file

@ -32,6 +32,7 @@ fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier
), ),
dismissButton = DialogButtonUM( dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(), title = state.dialog.dismissButtonText.resolveReference(),
warning = state.dialog.dismissWarningColor,
onClick = state.dialog.onDismissButtonClick, onClick = state.dialog.onDismissButtonClick,
), ),
onDismissDialog = state.dialog.onDismiss, onDismissDialog = state.dialog.onDismiss,

View file

@ -13,6 +13,7 @@ data class MultiWalletCreateWalletUM(
val description: TextReference, val description: TextReference,
val dismissButtonText: TextReference, val dismissButtonText: TextReference,
val confirmButtonText: TextReference, val confirmButtonText: TextReference,
val dismissWarningColor: Boolean = false,
val onConfirm: () -> Unit, val onConfirm: () -> Unit,
val onDismissButtonClick: () -> Unit, val onDismissButtonClick: () -> Unit,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,

View file

@ -28,6 +28,10 @@ internal class OnboardingMultiWalletModel @Inject constructor(
) )
init { init {
// TODO add analytics
// if (!manager.isActivationStarted(notNullCard.cardId)) {
// Analytics.send(Onboarding.Started())
// }
initScreenTitleSub() initScreenTitleSub()
} }
@ -49,11 +53,10 @@ internal class OnboardingMultiWalletModel @Inject constructor(
params.titleProvider.changeTitle(title) params.titleProvider.changeTitle(title)
modelScope.launch { modelScope.launch {
state state.map { it.currentStep }
.map { it.currentStep }
.collectLatest { step -> .collectLatest { step ->
val title = screenTitleByStep(step) val stepTitle = screenTitleByStep(step)
params.titleProvider.changeTitle(title) params.titleProvider.changeTitle(stepTitle)
} }
} }
} }

View file

@ -6,7 +6,6 @@ data class OnboardingMultiWalletState(
val currentStep: Step, val currentStep: Step,
val currentScanResponse: ScanResponse, val currentScanResponse: ScanResponse,
) { ) {
enum class Step { enum class Step {
CreateWallet, AddBackupDevice, FinishBackup, Done CreateWallet, AddBackupDevice, FinishBackup, Done
} }

View file

@ -0,0 +1,17 @@
package com.tangem.sdk.api
import androidx.activity.ComponentActivity
import com.tangem.TangemSdk
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.extensions.init
import java.lang.ref.WeakReference
class BackupServiceHolder {
lateinit var backupService: WeakReference<BackupService>
private set
fun createAndSetService(tangemSdk: TangemSdk, activity: ComponentActivity) {
backupService = WeakReference(BackupService.init(tangemSdk, activity))
}
}