Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-06 22:30:19 +03:00
parent 261a16113f
commit 5036f8a383
39 changed files with 1168 additions and 503 deletions

View file

@ -75,8 +75,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanNoteResponse != null
val isOnboardingStarted = store.state.onboardingState.onboardingData?.scanNoteResponse != null
if ((backStackIsEmpty || isScannedBefore) && !isOnboardingStarted) {
val isOnboardingServiceActive = store.state.globalState.onboardingService != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)

View file

@ -6,8 +6,7 @@ import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.products.note.OnboardingAddressInfoDialog
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -43,6 +42,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
dialog = when (state.dialog) {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
@ -71,9 +71,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
PersonalSignDialog.create(state.dialog.data, context)
is OnboardingAction.Dialog.AddressInfo -> {
OnboardingAddressInfoDialog(state.dialog, context)
}
else -> null
}
dialog?.show()

View file

@ -12,7 +12,7 @@ import com.tangem.tap.network.NetworkConnectivity
/**
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.loadWalletData(): Result<Wallet> = try {
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
update()
Result.Success(wallet)
} catch (exception: Exception) {
@ -23,10 +23,10 @@ suspend fun WalletManager.loadWalletData(): Result<Wallet> = try {
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
if (blockchain.isNoAccountError(exception) && amountToCreateAccount != null) {
Result.Failure(TapError.NoAccount(amountToCreateAccount.toString()))
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
} else {
val message = exception.localizedMessage ?: "Unknown exception during WalletManager update"
Result.Failure(TapError.CustomError(message))
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
}
}
}

View file

@ -8,7 +8,6 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.onboarding.redux.OnboardingReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
@ -22,7 +21,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingState = OnboardingReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),

View file

@ -16,8 +16,6 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMidd
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.redux.OnboardingMiddleware
import com.tangem.tap.features.onboarding.redux.OnboardingState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
@ -31,7 +29,6 @@ data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingState: OnboardingState = OnboardingState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val walletState: WalletState = WalletState(),
@ -48,7 +45,6 @@ data class AppState(
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
WalletMiddleware().walletMiddleware,

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.tap.features.wallet.redux.AddressData
/**
[REDACTED_AUTHOR]
*/
@ -7,4 +9,9 @@ interface StateDialog
sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val addressData: AddressData,
val onCopyAddress: () -> Unit,
val onExploreAddress: () -> Unit
) : AppDialog()
}

View file

@ -3,10 +3,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.common.redux.*
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -15,6 +12,7 @@ import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.service.ProductOnboardingService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.Action
@ -23,16 +21,21 @@ sealed class GlobalAction : Action {
// notifications
data class ShowNotification(override val messageResource: Int) : GlobalAction(), NotificationAction
data class ShowToastNotification(override val messageResource: Int) : GlobalAction(), ToastNotificationAction
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
data class DebugShowError(override val error: TapError) : GlobalAction(), DebugErrorAction
sealed class Onboarding {
data class Activate(val onboardingService: ProductOnboardingService) : GlobalAction()
object Deactivate : GlobalAction()
}
data class ReadCard(
val onSuccess: (suspend (ScanNoteResponse) -> Unit)? = null,
val onFailure: (suspend (TangemError) -> Unit)? = null,
val onSuccess: ((ScanNoteResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,
) : GlobalAction()

View file

@ -6,6 +6,7 @@ import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
@ -16,9 +17,7 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
class GlobalMiddleware {
@ -73,7 +72,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
@ -82,7 +81,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
)
}
}
@ -90,9 +89,9 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.ReadCard -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler, action.messageResId)
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId)
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
withMainContext {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)

View file

@ -10,6 +10,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Activate -> {
globalState.copy(onboardingService = action.onboardingService)
}
is GlobalAction.Onboarding.Deactivate -> {
globalState.copy(onboardingService = null)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}

View file

@ -8,6 +8,7 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.service.ProductOnboardingService
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.StateType
@ -24,7 +25,8 @@ data class GlobalState(
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null
val moonPayUserStatus: MoonPayUserStatus? = null,
val onboardingService: ProductOnboardingService? = null,
) : StateType
typealias CryptoCurrencyName = String

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain
import CreateProductWalletAndRescanTask
import android.content.Context
import com.tangem.Message
import com.tangem.TangemSdk
@ -22,6 +23,7 @@ import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
@ -31,8 +33,10 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
@Deprecated("Use can product")
suspend fun scanNote(
analyticsHandler: AnalyticsHandler, messageRes: Int? = null,
analyticsHandler: AnalyticsHandler,
messageRes: Int? = null,
): CompletionResult<ScanNoteResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
@ -42,6 +46,21 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
).also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler,
messageRes: Int? = null,
): CompletionResult<ScanNoteResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(ScanProductTask(), null, message)
.also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
suspend fun createProductWallet(cardId: String?): CompletionResult<Card> {
return runTaskAsync(CreateProductWalletAndRescanTask(), cardId, Message(context.getString(R.string.initial_message_create_wallet_body)))
}
private fun sendScanFailuresToAnalytics(
analyticsHandler: AnalyticsHandler,
result: CompletionResult<ScanNoteResponse>

View file

@ -6,7 +6,7 @@ import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.loadWalletData
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
@ -40,22 +40,17 @@ class TapWalletManager {
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
handleUpdateWalletResult(walletManager.loadWalletData(), walletManager)
handleUpdateWalletResult(walletManager.safeUpdate(), walletManager)
}
suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
val result = walletManager.safeUpdate()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.UpdateWallet.Success(result.data))
is Result.Failure ->
store.dispatch(WalletAction.UpdateWallet.Failure(result.error?.localizedMessage))
store.dispatch(WalletAction.UpdateWallet.Failure(result.error.localizedMessage))
}
}
@ -262,11 +257,11 @@ class TapWalletManager {
TapError.NoInternetConnection -> {
store.dispatch(WalletAction.LoadData.Failure(error))
}
is TapError.NoAccount -> {
is TapError.WalletManagerUpdate.NoAccountError -> {
store.dispatch(WalletAction.LoadWallet
.NoAccount(walletManager.wallet, error.amountToCreateAccount))
.NoAccount(walletManager.wallet, error.customMessage))
}
is TapError.CustomError -> {
is TapError.WalletManagerUpdate.InternalError -> {
store.dispatch(WalletAction.LoadWallet
.Failure(walletManager.wallet, error.customMessage))
}

View file

@ -60,6 +60,8 @@ object TapWorkarounds {
)
}
val DELAY_SDK_DIALOG_CLOSE = 1400L
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !isTangemNote()

View file

@ -67,7 +67,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
if (card.isTangemTwin()) {
dealWithTwinCard(card, session, callback)
} else if (card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
} else if (!card.isTangemNote() && card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
} else {
callback(CompletionResult.Success(

View file

@ -0,0 +1,139 @@
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.TapWorkarounds.isTangemWallet
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.twins.isTangemTwin
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletAndRescanTask : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
CreateProductWalletTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
private class CreateProductWalletTask : CardSessionRunnable<CreateWalletResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val commandProcessor = when {
card.isTangemNote() -> CreateWalletTangemNote()
card.isTangemTwin() -> CreateWalletTangemTwin()
card.isTangemWallet() -> CreateWalletTangemWallet()
else -> CreateWalletOtherCards()
}
commandProcessor.proceed(card, session, callback)
}
}
private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
if (card.supportedCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val curvesSupportedByBlockchain = card.getTangemNoteBlockchain()?.getSupportedCurves()
if (curvesSupportedByBlockchain == null || curvesSupportedByBlockchain.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val intersectCurves = card.supportedCurves.intersect(curvesSupportedByBlockchain).toList()
if (intersectCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val curve = if (intersectCurves.contains(EllipticCurve.Secp256k1)) {
EllipticCurve.Secp256k1
} else {
intersectCurves[0]
}
CreateWalletCommand(curve).run(session, callback)
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
throw UnsupportedOperationException()
}
}
private class CreateWalletTangemTwin : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
throw UnsupportedOperationException()
}
}
private class CreateWalletOtherCards : ProductCommandProcessor<CreateWalletResponse> {
private var index = 0
private val curves = EllipticCurve.values().toList()
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
val curve = curves[index]
createWallet(curve, session, callback)
}
private fun createWallet(
curve: EllipticCurve,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (index == curves.lastIndex) {
callback(CompletionResult.Success(result.data))
return@run
}
index += 1
createWallet(curves[index], session, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor<T> {
fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit,
)
}

View file

@ -0,0 +1,141 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.TapWorkarounds.isTangemWallet
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tasks.CreateWalletsTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTangemTwin
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwin() -> ScanTwinProcessor()
card.isTangemWallet() -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
return null
}
}
private class ScanNoteProcessor : ProductCommandProcessor<ScanNoteResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanNoteResponse(card, session.environment.walletData)))
}
}
private class ScanWalletProcessor : ProductCommandProcessor<ScanNoteResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanNoteResponse(card, session.environment.walletData)))
}
}
private class ScanTwinProcessor : ProductCommandProcessor<ScanNoteResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanNoteResponse(
card = card,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
}
}
}
}
private class ScanOtherCardsProcessor : ProductCommandProcessor<ScanNoteResponse> {
override fun proceed(card: Card, session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, walletData)))
return
}
val curvesPresent = card.wallets.map { it.curve }
val curvesToCreate = card.supportedCurves.subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, walletData)))
return
}
CreateWalletsTask().run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanNoteResponse(result.data, walletData)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import org.rekotlin.StoreSubscriber
import java.lang.ref.WeakReference
@ -40,10 +41,15 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
store.dispatch(HomeAction.SetFragmentShareTransition(shareTransition))
store.dispatch(HomeAction.Init)
btn_scan_card?.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_get_new_card?.setOnClickListener { store.dispatch(HomeAction.GoToShop) }
tv_header.setText(R.string.home_welcome_header)
tv_body.setText(R.string.home_welcome_body)
btnScanCard = IndeterminateProgressButtonWidget(btn_scan_card, progress)
btn_main_action.setText(R.string.home_button_scan)
btn_main_action.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_alternative_action.setText(R.string.home_button_get_new_card)
btn_alternative_action.setOnClickListener { store.dispatch(HomeAction.GoToShop) }
btnScanCard = IndeterminateProgressButtonWidget(btn_main_action, progress)
}
override fun onStart() {

View file

@ -1,17 +1,24 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.post
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingService
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.onboarding.service.OnboardingHelper
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class HomeMiddleware {
@ -32,13 +39,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
store.dispatch(GlobalAction.GetMoonPayUserStatus)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
}
is HomeAction.ReadCard -> {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
launchOnboardingService()
}
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.GoToShop -> store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
}
next(action)
@ -46,39 +47,59 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
}
}
private fun launchOnboardingService() {
fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
private fun handleReadCard() {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
fun proceedToWalletScreen(tapWalletManager: TapWalletManager, scanResponse: ScanNoteResponse) {
changeButtonState(ButtonState.ENABLED)
scope.launch {
tapWalletManager.onCardScanned(scanResponse)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ReadCard({ scanResponse ->
val tapWalletManager = store.state.globalState.tapWalletManager
if (!OnboardingHelper.isOnboardingCase(scanResponse)) {
proceedToWalletScreen(tapWalletManager, scanResponse)
return@ReadCard
}
val service = OnboardingHelper.createService(
AppScreen.Home,
scanResponse,
tapWalletManager.walletManagerFactory
)
if (service == null) {
proceedToWalletScreen(tapWalletManager, scanResponse)
return@ReadCard
}
service.onInitializationProgress = {
when (it) {
ProgressState.Loading -> changeButtonState(ButtonState.PROGRESS)
ProgressState.Done -> changeButtonState(ButtonState.ENABLED)
ProgressState.Error -> changeButtonState(ButtonState.ENABLED)
}
}
service.onInitialized = {
store.dispatch(GlobalAction.Onboarding.Activate(service))
store.dispatch(NavigationAction.NavigateTo(it, store.state.homeState.shareTransition))
}
service.onError = {
store.dispatchErrorNotification(it)
}
post(DELAY_SDK_DIALOG_CLOSE) { service.initializeOnboarding() }
}, {
changeButtonState(ButtonState.ENABLED)
}))
}
changeButtonState(ButtonState.PROGRESS)
val walletManager = store.state.globalState.tapWalletManager
val service = OnboardingService(
walletManager,
preferencesStorage.usedCardsPrefStorage
)
service.onReadyToProceed = {
store.dispatch(OnboardingAction.SetData(it))
changeButtonState(ButtonState.ENABLED)
val sharedTransition = store.state.homeState.shareTransition
store.dispatch(NavigationAction.NavigateTo(it.onboardingScreen, fragmentShareTransition = sharedTransition))
}
service.onFailedToProceedToOnboardingCase = {
changeButtonState(ButtonState.ENABLED)
walletManager.onCardScanned(it)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
service.onError = {
changeButtonState(ButtonState.ENABLED)
store.dispatch(GlobalAction.DebugShowError(TapError.CustomError(it)))
}
store.dispatch(GlobalAction.ReadCard(service::onSuccess, {
changeButtonState(ButtonState.ENABLED)
store.dispatch(GlobalAction.DebugShowError(TapError.ScanCardError))
}))
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.onboarding
import android.content.Context
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_onboarding_address_info.*
import kotlinx.android.synthetic.main.layout_pseudo_toolbar.*
/**
[REDACTED_AUTHOR]
*/
class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context
) : BottomSheetDialog(context) {
init {
this.setContentView(R.layout.dialog_onboarding_address_info)
setOnCancelListener { store.dispatchDialogHide() }
}
override fun show() {
super.show()
val data = stateDialog.addressData
imv_close.setOnClickListener {
dismissWithAnimation = true
cancel()
}
imv_qr_code.setImageBitmap(data.qrCode)
tv_address.text = data.address
btn_fl_copy_address.setOnClickListener {
context.copyToClipboard(data.address)
stateDialog.onCopyAddress()
}
btn_fl_explore.setOnClickListener {
stateDialog.onExploreAddress()
}
}
}

View file

@ -1,168 +0,0 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.TapWorkarounds.isTangemWallet
import com.tangem.tap.domain.UrlBitmapLoader
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.redux.OnboardingData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.persistence.UsedCardsPrefStorage
import com.tangem.tap.store
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class OnboardingService(
private val tapWalletManager: TapWalletManager,
private val cardInfoStorage: UsedCardsPrefStorage
) {
var onReadyToProceed: ((OnboardingData) -> Unit)? = null
set(value) {
field = value
onboardingDataLinker.onReady = { value?.invoke(it) }
}
var onFailedToProceedToOnboardingCase: (suspend (ScanNoteResponse) -> Unit)? = null
var onError: ((String) -> Unit)? = null
private val onboardingDataLinker = OnboardingDataLinker()
suspend fun onSuccess(response: ScanNoteResponse) {
onboardingDataLinker.scanNoteResponse = response
val card = response.card
val hasWallets = card.hasWallets()
if (!card.isTangemNote() && !card.isTangemWallet() && !card.isTangemTwin()) {
if (hasWallets) {
cardInfoStorage.activate(card.cardId)
onFailedToProceedToOnboardingCase?.invoke(response)
return
} else {
cardInfoStorage.activate(card.cardId) // вызвать только после того, как был создан кошелек
onboardingDataLinker.navigateTo = AppScreen.OnboardingOther
}
}
if (cardInfoStorage.wasActivated(card.cardId)) {
onFailedToProceedToOnboardingCase?.invoke(response)
return
}
loadCardImage(card) { onboardingDataLinker.cardArtwork = it }
if (hasWallets) {
val walletManagerFactory = tapWalletManager.walletManagerFactory
when {
card.isTangemNote() -> loadBalanceNote(walletManagerFactory, response)
card.isTangemWallet() -> loadBalanceWallet(walletManagerFactory, response)
card.isTangemTwin() -> loadBalanceTwins(walletManagerFactory, response)
}
} else {
onboardingDataLinker.valueOfWalletAmount = BigDecimal.ZERO
}
onboardingDataLinker.navigateTo = when {
card.isTangemNote() -> AppScreen.OnboardingNote
card.isTangemWallet() -> AppScreen.OnboardingWallet
card.isTangemTwin() -> AppScreen.OnboardingTwins
else -> AppScreen.OnboardingOther
}
}
private suspend fun loadBalanceNote(walletManagerFactory: WalletManagerFactory, response: ScanNoteResponse) {
val walletManager: WalletManager = walletManagerFactory.makePrimaryWalletManager(response).guard {
onError?.invoke("Can't proceed: primary wallet manager didn't create")
return
}
walletManager.update()
store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
onError?.invoke("Amount can't be NULL")
return
}
onboardingDataLinker.valueOfWalletAmount = valueOfAmount
}
private suspend fun loadBalanceWallet(walletManagerFactory: WalletManagerFactory, response: ScanNoteResponse) {
}
private suspend fun loadBalanceTwins(walletManagerFactory: WalletManagerFactory, response: ScanNoteResponse) {
}
private suspend fun loadCardImage(card: Card, callback: (Artwork?) -> Unit) {
fun loadBitmap(url: String, callback: (Artwork?) -> Unit) {
UrlBitmapLoader().loadBitmap(url) {
when (it) {
is Result.Success -> callback(Artwork(url, it.data))
is Result.Failure -> callback(null)
}
}
}
when (val cardInfoResult = OnlineCardVerifier().getCardInfo(card.cardId, card.cardPublicKey)) {
is Result.Success -> {
val artworkUrl = card.getArtworkUrl(cardInfoResult.data.artwork?.id) ?: Artwork.DEFAULT_IMG_URL
loadBitmap(artworkUrl, callback)
}
is Result.Failure -> loadBitmap(Artwork.DEFAULT_IMG_URL, callback)
}
}
}
class OnboardingDataLinker {
var onReady: ((OnboardingData) -> Unit)? = null
internal var scanNoteResponse: ScanNoteResponse? = null
set(value) {
field = value
tryToProceed()
}
internal var cardArtwork: Artwork? = null
set(value) {
field = value
tryToProceed()
}
internal var valueOfWalletAmount: BigDecimal? = null
set(value) {
field = value
tryToProceed()
}
internal var navigateTo: AppScreen? = null
set(value) {
field = value
tryToProceed()
}
private fun tryToProceed() {
if (!isReady()) return
val data = OnboardingData(scanNoteResponse!!, cardArtwork!!, valueOfWalletAmount, navigateTo!!)
onReady?.invoke(data)
}
private fun isReady(): Boolean {
val objList = mutableListOf(scanNoteResponse, cardArtwork, valueOfWalletAmount, navigateTo)
return !objList.any { it == null }
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.tap.features.onboarding.products
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_pseudo_toolbar.*
import kotlinx.android.synthetic.main.view_confetti.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
protected var fromScreen: AppScreen? = null
override fun onCreate(savedInstanceState: Bundle?) {
fromScreen = store.state.globalState.onboardingService?.fromScreen
super.onCreate(savedInstanceState)
}
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (fromScreen) {
AppScreen.Home -> {
enterTransition = HomeToOnboardingTransition()
exitTransition = HomeToOnboardingTransition()
}
AppScreen.Wallet -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onboarding_top_container.inflate(getOnboardingTopContainerId(), true)
configureToolbar()
}
private fun configureToolbar() {
when (fromScreen) {
AppScreen.Home -> {
toolbar.show(true)
pseudo_toolbar.visibility = View.GONE
}
AppScreen.Wallet -> {
toolbar.visibility = View.GONE
pseudo_toolbar.show(true)
imv_close.setOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
}
abstract fun getOnboardingTopContainerId(): Int
protected fun showConfetti(show: Boolean) {
lav_confetti.show(show)
if (show) {
lav_confetti.playAnimation()
} else {
lav_confetti.cancelAnimation()
}
}
}

View file

@ -2,46 +2,46 @@ package com.tangem.tap.features.onboarding.products.note
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.addOnBackPressedDispatcher
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.redux.OnboardingNoteStep
import com.tangem.tap.features.send.BaseStoreFragment
import androidx.transition.TransitionManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.postUi
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.*
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding.*
import org.rekotlin.StoreSubscriber
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
/**
[REDACTED_AUTHOR]
*/
class OnboardingNoteFragment : BaseStoreFragment(R.layout.fragment_onboarding), StoreSubscriber<OnboardingNoteState> {
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
activity?.addOnBackPressedDispatcher {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(OnboardingAction.SetInitialStepOfScreen(OnboardingNoteStep.None))
}
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(OnboardingNoteAction.Init)
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
val resources = AndroidResources(
OnboardingStringResources(
R.string.copy_toast_msg
)
)
imv_front_card.setImageBitmap(store.state.onboardingState.onboardingData?.cardArtwork?.artwork)
store.dispatch(OnboardingNoteAction.SetResources(resources))
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
@ -55,79 +55,107 @@ class OnboardingNoteFragment : BaseStoreFragment(R.layout.fragment_onboarding),
override fun newState(state: OnboardingNoteState) {
if (activity == null) return
state.artworkBitmap?.let { imv_front_card.setImageBitmap(it) }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
onboarding_main_container.beginDelayedTransition()
when (state.currentStep) {
OnboardingNoteStep.None -> setupNoneState(state)
OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state)
OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state)
OnboardingNoteStep.Done -> setupSuccessState(state)
OnboardingNoteStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
}
private fun setBalance(state: OnboardingNoteState) {
if (state.balanceCurrency.blockchain == Blockchain.Unknown) return
val balanceValue = state.balanceValue.stripZeroPlainString()
val currency = state.balanceCurrency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
}
private fun setupNoneState(state: OnboardingNoteState) {
pb_state.progress = state.progress
btn_main_action.isVisible = false
btn_alternative_action.isVisible = false
btnRefreshBalanceWidget.show(false)
tv_header.isVisible = false
tv_body.isVisible = false
btn_main_action.isVisible = true
btn_alternative_action.isVisible = true
btn_check_balance.isVisible = true
tv_header.isVisible = true
tv_body.isVisible = true
btn_main_action.text = "NULL"
btn_alternative_action.text = "NULL"
btn_check_balance.text = "NULL balance"
tv_header.text = "NULL"
tv_body.text = "NULL"
btn_main_action.text = ""
btn_alternative_action.text = ""
tv_header.text = ""
tv_body.text = ""
}
private fun setupCreateWalletState(state: OnboardingNoteState) {
pb_state.progress = state.progress
btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet)
btn_main_action.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) }
btn_alternative_action.setText(R.string.onboarding_button_what_does_it_mean)
btn_alternative_action.setOnClickListener { }
btn_check_balance.isVisible = false
btn_check_balance.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(R.layout.lp_onboarding_create_wallet)
}
private fun setupTopUpWalletState(state: OnboardingNoteState) {
pb_state.progress = state.progress
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener { store.dispatch(OnboardingNoteAction.TopUp) }
btn_main_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener { }
btn_check_balance.isVisible = false
btn_check_balance.setOnClickListener { }
btn_alternative_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
btnRefreshBalanceWidget.changeState(state.balanceState)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(R.layout.lp_onboarding_topup_wallet)
}
private fun setupSuccessState(state: OnboardingNoteState) {
pb_state.progress = state.progress
private fun setupDoneState(state: OnboardingNoteState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) }
btn_main_action.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.setText("")
btn_alternative_action.setOnClickListener { }
btn_check_balance.isVisible = false
btn_check_balance.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(R.layout.lp_onboarding_done)
}
private fun updateConstraints(@LayoutRes layoutId: Int) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
}
}

View file

@ -1,16 +1,31 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.service.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingNoteAction : Action {
object Init : OnboardingNoteAction()
// from user, ui
data class SetResources(val resources: AndroidResources) : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()
object ShowAddressInfoDialog : OnboardingNoteAction()
// from redux
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
data class SetStepOfScreen(val step: OnboardingNoteStep) : OnboardingNoteAction()
sealed class Balance {
object Update : OnboardingNoteAction()
data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction()
}
sealed class Confetti {
object Show : OnboardingNoteAction()
object Hide : OnboardingNoteAction()
}
}

View file

@ -1,9 +1,15 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.redux.OnboardingNoteStep
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.onboarding.service.balanceIsToppedUp
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -13,8 +19,6 @@ import org.rekotlin.Middleware
class OnboardingNoteMiddleware {
companion object {
private const val CARD_SHOP_URI =
"https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate"
val handler = onboardingNoteMiddleware
}
}
@ -22,51 +26,93 @@ class OnboardingNoteMiddleware {
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleMainOnboardingAction(action, ::handleNoteAction)
handleNoteAction(action)
next(action)
}
}
}
private fun handleMainOnboardingAction(action: Action, isNotMainAction: (Action) -> Unit) {
val action = action as? OnboardingAction ?: isNotMainAction(action)
when (action) {
}
}
private fun handleNoteAction(action: Action) {
val onboardingState = store.state.onboardingState
if (action !is OnboardingNoteAction) return
val service = store.state.onboardingNoteState.onboardingService ?: return
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.SetWalletManager -> {
}
is OnboardingNoteAction.Init -> {
val step = if (noteState.walletManager == null) {
OnboardingNoteStep.CreateWallet
} else {
OnboardingNoteStep.TopUpWallet
is OnboardingNoteAction.DetermineStepOfScreen -> {
val step = when {
service.walletManager == null -> OnboardingNoteStep.CreateWallet
service.balanceIsToppedUp() -> OnboardingNoteStep.Done
else -> OnboardingNoteStep.TopUpWallet
}
store.dispatch((OnboardingNoteAction.SetStepOfScreen(step)))
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step == OnboardingNoteStep.Done) {
service.activationFinished()
postUi(500) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
store.dispatch(OnboardingAction.SetInitialStepOfScreen(step))
}
is OnboardingNoteAction.CreateWallet -> {
scope.launch {
val scanNoteResponse = onboardingState.onboardingData?.scanNoteResponse
val result = tangemSdkManager.createWallet(scanNoteResponse?.card?.cardId)
when (result) {
is CompletionResult.Success -> {
// val scanNoteResponse = scanNoteResponse!!.copy(card = result.data)
// store.dispatch(OnboardingAction.SetScanNoteResponse(scanNoteResponse))
}
is CompletionResult.Failure -> {
store.dispatch(OnboardingAction.Error("Failed to create wallet"))
val result = tangemSdkManager.createProductWallet(service.scanResponse.card.cardId)
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = service.scanResponse.copy(card = result.data)
service.scanResponse = updatedResponse
service.activationStarted()
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
store.dispatch(OnboardingNoteAction.Balance.Update)
}
is CompletionResult.Failure -> {
// do nothing
}
}
}
}
}
is OnboardingNoteAction.TopUp -> {
is OnboardingNoteAction.Balance.Update -> {
val loadingBalance = service.getBalance().copy(
state = ProgressState.Loading,
error = null,
criticalError = null
)
store.dispatch(OnboardingNoteAction.Balance.Set(loadingBalance))
scope.launch {
val loadedBalance = service.updateBalance()
if (loadedBalance.criticalError != null) {
store.dispatchErrorNotification(loadedBalance.criticalError)
}
withMainContext { store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) }
}
}
is OnboardingNoteAction.Balance.Set -> {
if (service.balanceIsToppedUp()) {
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.onboardingService?.getAddressData() ?: return
val addressWasCopied = noteState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val topUpUrl = noteState.onboardingService?.getToUpUrl() ?: return
store.dispatchOpenUrl(topUpUrl)
}
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Deactivate)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(service.scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.redux.OnboardingNoteStep
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.service.OnboardingNoteService
import org.rekotlin.Action
class OnboardingNoteReducer {
@ -15,23 +15,46 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt
var state = appState.onboardingNoteState
when (action) {
is OnboardingAction.SetInitialStepOfScreen -> {
(action.step as? OnboardingNoteStep)?.let {
if (it != state.currentStep && state.steps.contains(it)) {
state = state.copy(currentStep = it)
}
is GlobalAction.Onboarding.Activate -> {
if (action.onboardingService is OnboardingNoteService) {
val service = action.onboardingService
val balance = service.getBalance()
state = state.copy(
onboardingService = service,
artworkBitmap = service.getArtwork().value?.artwork,
balanceValue = balance.value,
balanceCurrency = balance.currency,
balanceState = balance.state,
amountToCreateAccount = balance.amountToCreateAccount,
showConfetti = false
)
}
}
OnboardingAction.SwitchToNextStep -> {
val progress = state.progress
if (progress >= 0 && progress != state.steps.size - 1) {
state = state.copy(currentStep = state.steps[progress + 1])
GlobalAction.Onboarding.Deactivate -> {
state = OnboardingNoteState()
}
is OnboardingNoteAction.SetResources -> {
state = state.copy(resources = action.resources)
}
is OnboardingNoteAction.Balance.Set -> {
state = state.copy(
balanceValue = action.balance.value,
balanceCurrency = action.balance.currency,
balanceState = action.balance.state,
amountToCreateAccount = action.balance.amountToCreateAccount,
)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingNoteAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
is OnboardingNoteAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingNoteAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state

View file

@ -1,18 +1,42 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.redux.OnboardingNoteStep
import android.graphics.Bitmap
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.onboarding.service.OnboardingNoteService
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
val onboardingService: OnboardingNoteService? = null,
val resources: AndroidResources = AndroidResources(),
// UI
val showConfetti: Boolean = false,
val artworkBitmap: Bitmap? = null,
val balanceValue: BigDecimal = BigDecimal.ZERO,
val balanceCurrency: Currency = Currency.Blockchain(Blockchain.Unknown),
val balanceState: ProgressState = ProgressState.Done,
val amountToCreateAccount: String? = null,
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
}
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}
data class AndroidResources(
val strings: OnboardingStringResources = OnboardingStringResources()
)
data class OnboardingStringResources(
val addressWasCopied: Int = -1
)

View file

@ -1,31 +1,20 @@
package com.tangem.tap.features.onboarding.products.old
package com.tangem.tap.features.onboarding.products.otherCards
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.addOnBackPressedDispatcher
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.redux.OnboardingOtherStep
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsFragment : BaseStoreFragment(R.layout.fragment_onboarding), StoreSubscriber<OnboardingOtherCardsState> {
class OnboardingOtherCardsFragment : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<OnboardingOtherCardsState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.addOnBackPressedDispatcher {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(OnboardingAction.SetInitialStepOfScreen(OnboardingOtherStep.Done))
}
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -47,4 +36,6 @@ class OnboardingOtherCardsFragment : BaseStoreFragment(R.layout.fragment_onboard
if (activity == null) return
}
}
}
class OnboardingOtherCardsState {}

View file

@ -3,11 +3,7 @@ package com.tangem.tap.features.onboarding.products.wallet
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.addOnBackPressedDispatcher
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.redux.OnboardingAction
import com.tangem.tap.features.onboarding.redux.OnboardingOtherStep
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.store
import com.tangem.wallet.R
@ -21,11 +17,6 @@ class OnboardingWalletFragment : BaseStoreFragment(R.layout.fragment_onboarding_
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.addOnBackPressedDispatcher {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(OnboardingAction.SetInitialStepOfScreen(OnboardingOtherStep.Done))
}
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -33,8 +24,6 @@ class OnboardingWalletFragment : BaseStoreFragment(R.layout.fragment_onboarding_
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(OnboardingAction.OnboardingOtherAction.Init)
}
override fun subscribeToStore() {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.tap.features.onboarding.redux.OnboardingStep
import org.rekotlin.StateType
/**
@ -10,6 +9,6 @@ data class OnboardingWalletState(
val any: String? = null
) : StateType
enum class OnboardingWalletStep : OnboardingStep {
enum class OnboardingWalletStep {
None, CreateWallet, TopUpWallet, Backup, Done
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.onboarding.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingAction : Action {
data class SetInitialStepOfScreen(val step: OnboardingStep) : OnboardingAction()
object SwitchToNextStep : OnboardingAction()
data class SetData(val data: OnboardingData) : OnboardingAction()
sealed class OnboardingOtherAction : Action {
object Init : OnboardingOtherAction()
object CreateWallet : OnboardingOtherAction()
object Done : OnboardingOtherAction()
}
class Error(val message: String) : OnboardingAction()
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.onboarding.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
class OnboardingMiddleware {
companion object {
val handler = onboardingMiddleware
}
}
private val onboardingMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
val onboardingState = store.state.onboardingState
when (action) {
}
next(action)
}
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.onboarding.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class OnboardingReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, appState: AppState): OnboardingState {
if (action !is OnboardingAction) return appState.onboardingState
var state = appState.onboardingState
when (action) {
is OnboardingAction.SetData -> {
state = state.copy(onboardingData = action.data)
}
}
return state
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.onboarding.redux
import org.rekotlin.StateType
data class OnboardingState(
val onboardingData: OnboardingData? = null,
) : StateType
data class OnboardingOtherCardsState(
val any: String? = null
) : StateType

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.onboarding.redux
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.wallet.redux.Artwork
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class OnboardingData(
val scanNoteResponse: ScanNoteResponse,
val cardArtwork: Artwork,
val balance: BigDecimal?,
val onboardingScreen: AppScreen
)
interface OnboardingStep
enum class OnboardingNoteStep : OnboardingStep {
None, CreateWallet, TopUpWallet, Done
}
enum class OnboardingOtherStep : OnboardingStep {
CreateWallet, Done
}

View file

@ -0,0 +1,33 @@
package com.tangem.tap.features.onboarding.service
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingHelper {
companion object {
fun isOnboardingCase(response: ScanNoteResponse): Boolean = if (response.card.hasWallets())
preferencesStorage.usedCardsPrefStorage.activationIsStarted(response.card.cardId) else true
fun createService(
fromScreen: AppScreen,
scanResponse: ScanNoteResponse,
walletManagerFactory: WalletManagerFactory,
): ProductOnboardingService? {
val card = scanResponse.card
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
return when {
card.isTangemNote() -> OnboardingNoteService(fromScreen, scanResponse, cardInfoStorage,
walletManagerFactory)
else -> null
}
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.onboarding.service
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.persistence.UsedCardsPrefStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingNoteService(
override val fromScreen: AppScreen,
override var scanResponse: ScanNoteResponse,
cardInfoStorage: UsedCardsPrefStorage,
walletManagerFactory: WalletManagerFactory,
) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
override fun navigateToScreen(): AppScreen {
return AppScreen.OnboardingNote
}
}

View file

@ -0,0 +1,295 @@
package com.tangem.tap.features.onboarding.service
import android.util.Log
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.UrlBitmapLoader
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.persistence.UsedCardsPrefStorage
import com.tangem.tap.store
import kotlinx.coroutines.*
import java.io.PrintWriter
import java.io.StringWriter
import java.math.BigDecimal
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
[REDACTED_AUTHOR]
*/
interface OnboardingService {
val fromScreen: AppScreen
val scanResponse: ScanNoteResponse
// val cardInfoStorage: UsedCardsPrefStorage
var walletManager: WalletManager?
var onInitializationProgress: ((ProgressState) -> Unit)?
var onInitialized: ((AppScreen) -> Unit)?
var onError: ((TapError) -> Unit)?
fun initializeOnboarding()
fun getArtwork(): OnboardingArtwork
fun getBalance(): OnboardingWalletBalance
suspend fun updateBalance(): OnboardingWalletBalance
fun getToUpUrl(): String?
fun getAddressData(): AddressData?
fun activationStarted()
fun activationFinished()
}
abstract class ProductOnboardingService(
private val cardInfoStorage: UsedCardsPrefStorage,
private val walletManagerFactory: WalletManagerFactory,
) : OnboardingService {
override var walletManager: WalletManager? = null
override var onInitializationProgress: ((ProgressState) -> Unit)? = null
override var onInitialized: ((AppScreen) -> Unit)? = null
override var onError: ((TapError) -> Unit)? = null
protected var isProceeded = false
protected var loadedCardArtwork: OnboardingArtwork = OnboardingArtwork.loading()
set(value) {
field = value
tryToProceed()
}
protected var loadedBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading()
set(value) {
field = value
tryToProceed()
}
protected val internalScope = CoroutineScope(Job() + Dispatchers.IO + coroutineExceptionHandler())
override fun initializeOnboarding() {
val card = scanResponse.card
onInitializationProgress?.invoke(ProgressState.Loading)
internalScope.launch {
val artwork = loadCardArtwork(card)
withMainContext {
loadedCardArtwork = if (artwork == null) OnboardingArtwork.error() else OnboardingArtwork.done(artwork)
}
}
internalScope.launch { updateBalance() }
}
override fun getArtwork(): OnboardingArtwork = loadedCardArtwork
override fun getBalance(): OnboardingWalletBalance = loadedBalance
override fun getToUpUrl(): String? {
val wallet = walletManager?.wallet ?: return null
val config = store.state.globalState.configManager?.config ?: return null
val defaultAddress = wallet.address
return TradeCryptoHelper.getUrl(
TradeCryptoHelper.Action.Buy,
wallet.blockchain,
wallet.blockchain.currency,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
}
override fun getAddressData(): AddressData? {
val wallet = walletManager?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}
protected suspend fun loadCardArtwork(card: Card): Artwork? {
return when (val cardInfoResult = OnlineCardVerifier().getCardInfo(card.cardId, card.cardPublicKey)) {
is Result.Success -> {
val artworkUrl = card.getArtworkUrl(cardInfoResult.data.artwork?.id) ?: Artwork.DEFAULT_IMG_URL
loadBitmap(artworkUrl)
}
is Result.Failure -> {
loadBitmap(Artwork.DEFAULT_IMG_URL)
}
}
}
override suspend fun updateBalance(): OnboardingWalletBalance {
withMainContext { loadedBalance = OnboardingWalletBalance.loading(loadedBalance.value) }
val walletManager = walletManager ?: walletManagerFactory.makePrimaryWalletManager(scanResponse).guard {
val customError = TapError.CustomError("Loading cancelled. Cause: wallet manager didn't created")
loadedBalance = OnboardingWalletBalance.error(customError)
return loadedBalance
}
this.walletManager = walletManager
val currency = Currency.Blockchain(walletManager.wallet.blockchain)
val updatedBalance = when (val result = walletManager.safeUpdate()) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, hasForIncomingTransactions(), currency)
} else {
OnboardingWalletBalance.done(valueOfAmount, false, currency)
}
balance
}
}
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManagerUpdate.InternalError
else -> OnboardingWalletBalance.criticalError(error)
}
}
}
withMainContext { loadedBalance = updatedBalance }
return loadedBalance
}
override fun activationStarted() {
cardInfoStorage.activationStarted(scanResponse.card.cardId)
}
override fun activationFinished() {
cardInfoStorage.activationFinished(scanResponse.card.cardId)
}
protected open suspend fun hasForIncomingTransactions(): Boolean {
return false
}
protected open fun tryToProceed() {
if (isProceeded) return
loadedBalance.criticalError?.let {
sendError(it)
return
}
if (!isReadyToProceed()) return
isProceeded = true
onInitializationProgress?.invoke(ProgressState.Done)
onInitialized?.invoke(navigateToScreen())
}
protected open fun isReadyToProceed(): Boolean {
return loadedBalance.state != ProgressState.Loading && loadedCardArtwork.state != ProgressState.Loading
}
protected fun sendError(error: TapError) {
onInitializationProgress?.invoke(ProgressState.Error)
onError?.invoke(error)
}
protected abstract fun navigateToScreen(): AppScreen
private suspend fun loadBitmap(url: String): Artwork? {
return withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
UrlBitmapLoader().loadBitmap(url) {
val result = when (it) {
is Result.Success -> Artwork(url, it.data)
is Result.Failure -> null
}
continuation.resume(result)
}
}
}
}
private fun coroutineExceptionHandler(): CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
StringWriter().apply {
throwable.printStackTrace(PrintWriter(this))
Log.e("Coroutine", this.toString())
store.dispatchDebugErrorNotification(this.toString())
}
}
}
data class OnboardingArtwork(
val value: Artwork? = null,
val state: ProgressState
) {
companion object {
// balance was not loaded at all when any error occurred
fun error(): OnboardingArtwork = OnboardingArtwork(state = ProgressState.Error)
// balance is loading
fun loading(): OnboardingArtwork = OnboardingArtwork(state = ProgressState.Loading)
// balance was loaded
fun done(artwork: Artwork): OnboardingArtwork = OnboardingArtwork(artwork, ProgressState.Done)
}
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading
)
fun done(value: BigDecimal, hasTransactions: Boolean, currency: Currency.Blockchain): OnboardingWalletBalance =
OnboardingWalletBalance(value, currency, hasTransactions, ProgressState.Done)
}
}
fun ProductOnboardingService.balanceIsToppedUp(): Boolean {
val balance = getBalance()
return balance.value.isPositive() || balance.hasIncomingTransaction
}

View file

@ -24,7 +24,10 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureTransitions()
}
protected open fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)