Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-17 20:02:36 +05:00
parent d0c4d92a8c
commit 5fea60f2a2
83 changed files with 2801 additions and 775 deletions

View file

@ -6,13 +6,13 @@ apply plugin: 'com.google.firebase.crashlytics'
apply from: '../dependencies.gradle'
android {
compileSdkVersion 32
compileSdkVersion 33
defaultConfig {
applicationId "com.tangem.wallet"
versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1
versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT"
minSdkVersion 21
targetSdkVersion 32
targetSdkVersion 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
@ -83,11 +83,11 @@ dependencies {
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.play:core:1.10.3'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
@ -97,15 +97,15 @@ dependencies {
implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}"
// WebView
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.browser:browser:1.4.0"
implementation 'com.jakewharton.timber:timber:4.7.1'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "org.rekotlin:rekotlin:1.0.4"
@ -176,6 +176,9 @@ dependencies {
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
// Shared preferences (encrypted)
implementation 'at.favre.lib:armadillo:0.9.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View file

@ -1,11 +1,25 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -20,7 +34,7 @@ import com.tangem.domain.DomainDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -61,7 +75,7 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
when (val dialog = dialogState.value) {
is DomainDialog.DialogError -> ErrorDialog(
title = stringResource(id = R.string.common_error),
body = errorConverter.convert(dialog.error),
body = errorConverter.convert(dialog.error).message,
onDismissRequest
)
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)

View file

@ -1,6 +1,11 @@
package com.tangem.tap.common.compose
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -26,7 +31,7 @@ import com.tangem.common.module.ModuleError
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.CompositionLogger
import com.tangem.tap.common.compose.extensions.stringResourceDefault
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
/**
[REDACTED_AUTHOR]
@ -67,7 +72,7 @@ fun OutlinedTextFieldWidget(
debounce = debounceTextChanges,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
onTextChanged = onTextChanged
onTextChanged = onTextChanged,
)
errorConverter?.let { AnimatedErrorView(error, it) }
}
@ -105,7 +110,8 @@ private fun OutlinedProgressTextField(
onValueChanged = {
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
onTextChanged(it)
})
},
)
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]")
@ -188,7 +194,10 @@ private fun AnimatedErrorView(
exit = slideOutVertically() + fadeOut(),
) {
error?.let {
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
ErrorView(
text = errorConverter.convert(it).message,
style = TextStyle(fontSize = 14.sp),
)
}
}
}
@ -203,15 +212,13 @@ fun OutlinedTextFieldWithErrorTest() {
override val code: Int = 1,
override val message: String = "Error message",
override val data: Any? = null,
) : ModuleError
) : ModuleError()
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
Scaffold(
) {
Column(
) {
Scaffold {
Column {
OutlinedTextFieldWidget(
modifier = modifier,
fieldData = Field.Data("", false),

View file

@ -153,7 +153,7 @@ private val tangemPinConfig = PinViewConfig(
.width(42.dp)
.height(56.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFF5F5F5)),
.background(Color(0xFFF0F0F0)),
pinBoxPadding = 6.dp,
pinTextModifier = Modifier,
pinsCount = 4,

View file

@ -13,7 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.common.module.ModuleMessage
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
/**
@ -42,7 +42,7 @@ fun AddCustomTokenWarning(
)
SpacerH8()
Text(
text = converter.convert(warning),
text = converter.convert(warning).message,
color = colorResource(id = R.color.white),
fontSize = 13.sp,
lineHeight = 18.sp

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
fun Context.readFile(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
@ -13,4 +15,8 @@ fun Context.rewriteFile(content: String, fileName: String) {
fun Context.readAssetAsString(fileName: String): String {
return this.assets.open("$fileName.json").bufferedReader().readText()
}
fun Context.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}

View file

@ -13,16 +13,57 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Store
/**
* Dispatch action with creating the new coroutine with the Main dispatcher
*/
fun Store<*>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
store.dispatch(action)
}
}
fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
fun Store<*>.dispatchToastNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.ShowErrorNotification(error))
}
/**
* @param fatal used to indicate errors that should not normally occur
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.DebugShowErrorNotification(error))
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
dispatchOnMain(GlobalAction.ShowDialog(dialog))
}
fun Store<*>.dispatchDialogHide() {
dispatchOnMain(GlobalAction.HideDialog)
}
/**
* Dispatch action inside a coroutine with the Main dispatcher
*/
suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
/**
* Dispatch action
*/
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
}
@ -33,49 +74,4 @@ fun Store<*>.dispatchOpenUrl(url: String) {
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}
fun Store<*>.dispatchNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowNotification(resId))
}
}
fun Store<*>.dispatchToastNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowToastNotification(resId))
}
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowErrorNotification(error))
}
}
/**
* @param fatal used to indicate errors that should not normally have occurred
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.DebugShowErrorNotification(error))
}
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowDialog(dialog))
}
}
fun Store<*>.dispatchDialogHide() {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.HideDialog)
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
@ -69,4 +70,12 @@ fun WalletManager?.getAddressData(): AddressData? {
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}
fun<T> WalletManager.Companion.stub(): T {
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
return object : WalletManager(wallet) {
override val currentHost: String = ""
override suspend fun update() {}
} as T
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.common.moduleMessage
import android.content.Context
import com.tangem.common.module.ModuleMessage
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainModuleMessage
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
class ModuleMessageConverter(
private val context: Context
) : ModuleMessageConverter<ModuleMessage, String> {
override fun convert(message: ModuleMessage): String {
val convertedMessage = when (message) {
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
else -> null
}
return convertedMessage ?: convertUnknownMessage(message)
}
private fun convertUnknownMessage(message: ModuleMessage): String {
return "Unknown message: ${message::class.java.simpleName}"
}
}

View file

@ -1,48 +0,0 @@
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainError
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class DomainErrorConverter(
private val context: Context
) : ModuleMessageConverter<DomainError, String?> {
override fun convert(message: DomainError): String? = when (message) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
else -> null
}
}
private class AddCustomTokenConverter(
private val context: Context
) : ModuleMessageConverter<DomainError, String?> {
override fun convert(message: DomainError): String? {
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
val rawMessage = when (customTokenError) {
AddCustomTokenError.Warning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenError.Warning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.Warning.UnsupportedSolanaToken -> R.string.alert_manage_tokens_unsupported_message
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
AddCustomTokenError.InvalidDecimalsCount -> {
context.getString(R.string.custom_token_creation_error_wrong_decimals, 30)
}
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
else -> null
}
return when (rawMessage) {
is Int -> context.getString(rawMessage)
is String -> rawMessage
else -> null
}
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainError
import com.tangem.domain.DomainModuleMessage
/**
[REDACTED_AUTHOR]
*/
class DomainMessageConverter(
private val context: Context
) : ModuleMessageConverter<DomainModuleMessage, String?> {
override fun convert(message: DomainModuleMessage): String? {
return when (message) {
is DomainError -> DomainErrorConverter(context).convert(message)
else -> null
}
}
}

View file

@ -24,6 +24,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
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.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
@ -66,6 +67,7 @@ data class AppState(
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingSaltPayMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,

View file

@ -12,6 +12,7 @@ val logMiddleware: Middleware<AppState> = { dispatch, appState ->
{ action ->
if (LogConfig.storeAction) {
Timber.d("Dispatch action: $action")
// printOnboardingWalletState()
}
nextDispatch(action)
}

View file

@ -36,9 +36,10 @@ sealed class GlobalAction : Action {
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction()
object Stop : GlobalAction()
sealed class Onboarding : GlobalAction() {
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
data class StartForUnfinishedBackup(val isSaltPayBackup: Boolean) : Onboarding()
object Stop : Onboarding()
}
data class ScanCard(

View file

@ -18,14 +18,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(resources = action.resources)
}
is GlobalAction.Onboarding.Start -> {
val onboardingManager = if (action.scanResponse != null) {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
} else {
null
}
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
globalState.copy(onboardingState = OnboardingState(true, null))
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingState = OnboardingState(false))
}

View file

@ -11,17 +11,17 @@ import com.tangem.tap.features.wallet.redux.ProgressState
[REDACTED_AUTHOR]
*/
open class IndeterminateProgressButtonWidget(
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.Done
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.Done,
) : ViewStateWidget {
private var initialButtonText: CharSequence = button.text
private var icon: Drawable? = null
private var text: CharSequence = button.text
private var icon: Drawable? = button.icon
private var iconGravity: Int? = button.iconGravity
init {
icon = button.icon
changeState(initialState)
if (initialState != ProgressState.Done) changeState(initialState)
}
override val mainView: View = button
@ -37,15 +37,18 @@ open class IndeterminateProgressButtonWidget(
protected open fun switchToNone() {
button.isClickable = true
button.text = text
button.icon = icon
button.text = initialButtonText
iconGravity?.let { button.iconGravity = it }
progress.hide()
}
protected open fun switchToProgress() {
button.isClickable = false
button.icon = null
button.text = ""
button.icon = null
progress.show()
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain
import CreateProductWalletTask
import CreateProductWalletTaskResponse
import android.content.Context
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
@ -177,6 +178,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
}
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
return context.getString(stringResId, formatArgs)
}
companion object {
val config = Config(
linkedTerminal = true,

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.domain.configurable.Loader
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
/**
[REDACTED_AUTHOR]
@ -22,6 +23,7 @@ data class Config(
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val zendesk: ZendeskConfig? = null,
val saltPayConfig: SaltPayConfig? = null,
)
class ConfigManager {
@ -90,6 +92,7 @@ class ConfigManager {
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
saltPayConfig = values.saltPay,
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
@ -106,6 +109,7 @@ class ConfigManager {
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
saltPayConfig = values.saltPay,
)
}

View file

@ -2,32 +2,34 @@ package com.tangem.tap.domain.configurable.config
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
/**
[REDACTED_AUTHOR]
*/
class FeatureModel(
val isWalletPayIdEnabled: Boolean,
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
val isWalletPayIdEnabled: Boolean,
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
)
class ConfigValueModel(
val coinMarketCapKey: String,
val mercuryoWidgetId: String,
val mercuryoSecret: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockchairAuthorizationToken: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
val tronGridApiKey: String,
val coinMarketCapKey: String,
val mercuryoWidgetId: String,
val mercuryoSecret: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockchairAuthorizationToken: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
val saltPay: SaltPayConfig,
val tronGridApiKey: String,
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

@ -24,7 +24,7 @@ val Card.isWalletDataSupported: Boolean
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !isTangemNote() && !isSaltPay
return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -1,16 +1,20 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
@ -20,7 +24,7 @@ import com.tangem.tap.features.wallet.models.Currency
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse,
blockchain: Blockchain,
derivationParams: DerivationParams?
derivationParams: DerivationParams?,
): WalletManager? {
val card = scanResponse.card
if (card.isTestCard && blockchain.getTestnetVersion() == null) return null
@ -62,26 +66,26 @@ fun WalletManagerFactory.makeWalletManagerForApp(
makeWalletManager(
blockchain = environmentBlockchain,
walletPublicKey = wallet.publicKey,
curve = wallet.curve
curve = wallet.curve,
)
}
}
}
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork
scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork,
): WalletManager? {
return makeWalletManagerForApp(
scanResponse,
blockchain = blockchainNetwork.blockchain,
derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card)
derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card),
)
}
private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
return derivationPath?.let {
DerivationParams.Custom(
DerivationPath(it)
DerivationPath(it),
)
} ?: if (!card.settings.isHDWalletAllowed) {
null
@ -93,17 +97,19 @@ private fun getDerivationParams(derivationPath: String?, card: Card): Derivation
}
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse, currency: Currency
scanResponse: ScanResponse,
currency: Currency,
): WalletManager? {
return makeWalletManagerForApp(
scanResponse,
blockchain = currency.blockchain,
derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card)
derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card),
)
}
fun WalletManagerFactory.makeWalletManagersForApp(
scanResponse: ScanResponse, blockchains: List<Currency>,
scanResponse: ScanResponse,
blockchains: List<Currency>,
): List<WalletManager> {
return blockchains
.filter { it.isBlockchain() }
@ -122,7 +128,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
return makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = derivationParams
derivationParams = derivationParams,
)
}
@ -132,4 +138,23 @@ private fun selectWallet(wallets: List<CardWallet>): CardWallet? {
1 -> wallets[0]
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
}
}
fun WalletManagerFactory.makeSaltPayWalletManager(
scanResponse: ScanResponse,
): EthereumWalletManager {
val blockchain = scanResponse.getBlockchain()
if (blockchain != Blockchain.SaltPay && blockchain != Blockchain.SaltPayTestnet)
throw IllegalArgumentException()
val token = SaltPayWorkaround.tokenFrom(blockchain)
val cardWallet = scanResponse.card.wallets.firstOrNull().guard {
throw NullPointerException("SaltPay card must have one wallet at least")
}
return makeWalletManager(
blockchain = blockchain,
publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null),
tokens = listOf(token),
) as EthereumWalletManager
}

View file

@ -0,0 +1,88 @@
package com.tangem.tap.domain.statePrinter
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.redux.state.StringStateConverter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.store
import timber.log.Timber
import java.math.BigInteger
/**
[REDACTED_AUTHOR]
*/
class OnboardingWalletStateConverter : StringStateConverter<AppState> {
private val converter = MoshiJsonConverter(
MoshiJsonConverter.getTangemSdkAdapters() + internalAdapters(),
MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
private fun internalAdapters(): List<Any> {
return listOf(
BackupStepAdapter(),
BigIntegerAdapter(),
)
}
override fun convert(stateHolder: AppState): String {
val model = stateHolder.onboardingWalletState
val json = converter.prettyPrint(model)
return json
}
}
fun printOnboardingWalletState() {
val stringState = OnboardingWalletStateConverter().convert(store.state)
Timber.d(stringState)
}
class BackupStepAdapter {
@ToJson
fun toJson(src: BackupStep): String {
return when (src) {
BackupStep.AddBackupCards -> "AddBackupCards"
BackupStep.EnterAccessCode -> "EnterAccessCode"
BackupStep.Finished -> "Finished"
BackupStep.InitBackup -> "InitBackup"
BackupStep.ReenterAccessCode -> "ReenterAccessCode"
BackupStep.ScanOriginCard -> "ScanOriginCard"
BackupStep.SetAccessCode -> "SetAccessCode"
is BackupStep.WriteBackupCard -> "WriteBackupCard[${src.cardNumber}]"
BackupStep.WritePrimaryCard -> "WritePrimaryCard"
}
}
@FromJson
fun fromJson(json: String): BackupStep {
return when (json) {
"AddBackupCards" -> BackupStep.AddBackupCards
"EnterAccessCode" -> BackupStep.EnterAccessCode
"Finished" -> BackupStep.Finished
"InitBackup" -> BackupStep.InitBackup
"ReenterAccessCode" -> BackupStep.ReenterAccessCode
"ScanOriginCard" -> BackupStep.ScanOriginCard
"SetAccessCode" -> BackupStep.SetAccessCode
"WriteBackupCard[1]" -> BackupStep.WriteBackupCard(1)
"WriteBackupCard[2]" -> BackupStep.WriteBackupCard(2)
"WriteBackupCard[3]" -> BackupStep.WriteBackupCard(3)
"WritePrimaryCard" -> BackupStep.WritePrimaryCard
else -> throw UnsupportedOperationException()
}
}
}
class BigIntegerAdapter {
@ToJson
fun toJson(src: BigInteger): String {
return src.toString()
}
@FromJson
fun fromJson(json: String): BigInteger {
return BigInteger(json)
}
}

View file

@ -18,9 +18,9 @@ import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.isTangemTwins
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.ScanTask
@ -31,10 +31,9 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.extensions.hasNoWallets
import com.tangem.tap.domain.extensions.isHdWalletAllowedByApp
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.preferencesStorage
@ -60,7 +59,7 @@ class ScanProductTask(
}
val commandProcessor = when {
card.isTangemTwins() -> ScanTwinProcessor()
card.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
commandProcessor.proceed(card, session) { processorResult ->
@ -84,8 +83,8 @@ class ScanProductTask(
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease()) return TapSdkError.CardNotSupportedByRelease
if (card.isExcluded) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
if (card.isSaltPay && card.hasNoWallets()) return TapSdkError.ScanPrimaryCard
return null
}
@ -111,6 +110,7 @@ private class ScanWalletProcessor(
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.wallets.isEmpty() || !card.isMultiwalletAllowed) {
// match for the saltPay card to
startLinkingForBackupIfNeeded(card, session, callback)
return
}
@ -166,13 +166,17 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val productType = when(card.isSaltPay){
true -> ProductType.SaltPay
else -> ProductType.Wallet
}
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
productType = productType,
walletData = session.environment.walletData,
primaryCard = primaryCard,
),
@ -186,7 +190,7 @@ private class ScanWalletProcessor(
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
productType = productType,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard,

View file

@ -93,17 +93,10 @@ class UserTokensRepository(
private fun Card.getUserId(): String {
val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
return calculateUserId(walletPublicKey)
}
private fun calculateUserId(walletPublicKey: ByteArray): String {
val message = MESSAGE.toByteArray()
val keyHash = walletPublicKey.calculateSha256()
return message.calculateHmacSha256(keyHash).toHexString()
return UserWalletId(walletPublicKey).stringValue
}
companion object {
const val MESSAGE = "UserWalletID"
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
val fileReader = AndroidFileReader(context)
val oldUserTokensRepository = OldUserTokensRepository(
@ -114,4 +107,16 @@ class UserTokensRepository(
return UserTokensRepository(storageService, networkService)
}
}
}
data class UserWalletId(
val walletPublicKey: ByteArray,
) {
val stringValue: String = calculateUserId(walletPublicKey)
private fun calculateUserId(walletPublicKey: ByteArray): String {
val message = "UserWalletID".toByteArray()
val keyHash = walletPublicKey.calculateSha256()
return message.calculateHmacSha256(keyHash).toHexString()
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.common.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchNotification
@ -69,13 +69,8 @@ class DetailsMiddleware {
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(
GlobalAction.Onboarding.Start(
it,
fromHomeScreen = false,
),
)
}
}
DetailsAction.ScanCard -> {
@ -98,7 +93,7 @@ class DetailsMiddleware {
when (action) {
is DetailsAction.ResetToFactory.Start -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins()) {
if (card.isTangemTwins) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {

View file

@ -79,7 +79,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
}
}
val allowedSecurityOptions = when {
card.isStart2Coin || card.isTangemNote() || card.isSaltPay -> {
card.isStart2Coin || card.isTangemNote || card.isSaltPay -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
@ -97,7 +97,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
val notAllowedByCard = notAllowedByAnyWallet ||
(card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed)) ||
(card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) ||
card.isSaltPay
return !notAllowedByCard
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.cardsettings
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
@ -20,7 +20,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
},
)
} else {
val cardId = if (state.card.isTangemTwins()) {
val cardId = if (state.card.isTangemTwins) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
@ -30,7 +30,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
CardInfo.Issuer(state.cardInfo.issuer),
)
if (!state.card.isTangemTwins()) {
if (!state.card.isTangemTwins) {
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
}
cardDetails.add(

View file

@ -81,16 +81,12 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = {
store.dispatch(
HomeAction.GoToShop(store.state.globalState.userCountryCode)
)
},
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) },
onSearchTokensClick = {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
}
},
)
}

View file

@ -49,7 +49,6 @@ import com.tangem.tap.features.home.compose.content.StoriesWeb3
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
import kotlin.math.max
@ -74,7 +73,7 @@ fun StoriesScreen(
}
val isPressed = remember { mutableStateOf(false) }
val isPaused = isPressed.value || homeState.value.btnScanState.progressState == ProgressState.Loading
val isPaused = isPressed.value || homeState.value.scanInProgress
val hideContent = remember { mutableStateOf(true) }
@ -88,10 +87,10 @@ fun StoriesScreen(
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF090E13))
.background(Color(0xFF090E13)),
) {
Row(
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
) {
Box(
Modifier
@ -109,7 +108,7 @@ fun StoriesScreen(
isPressed.value = false
},
)
}
},
)
Box(
Modifier
@ -127,7 +126,7 @@ fun StoriesScreen(
isPressed.value = false
},
)
}
},
)
}
if (!isDarkBackground) {
@ -135,7 +134,7 @@ fun StoriesScreen(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_overlay),
contentDescription = null,
contentScale = ContentScale.FillBounds
contentScale = ContentScale.FillBounds,
)
}
@ -165,7 +164,7 @@ fun StoriesScreen(
.height(17.dp)
.alpha(if (hideContent.value) 0f else 1f)
.align(Alignment.Start),
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black),
)
when (currentStep.value) {
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
@ -190,27 +189,28 @@ fun StoriesScreen(
.height(48.dp),
colors = ButtonDefaults.textButtonColors(
backgroundColor = Color.White,
contentColor = Color(0xFF080C10)
)
contentColor = Color(0xFF080C10),
),
) {
Image(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = null
contentDescription = null,
)
Text(
text = stringResource(id = R.string.search_tokens_title),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
}
HomeButtons(
isDarkBackground = isDarkBackground,
modifier = Modifier
.padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp)
.fillMaxWidth(),
isDarkBackground = isDarkBackground,
btnScanStateInProgress = homeState.value.btnScanStateInProgress,
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick
onShopButtonClick = onShopButtonClick,
)
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -19,53 +21,89 @@ import com.tangem.wallet.R
@Composable
fun HomeButtons(
isDarkBackground: Boolean, modifier: Modifier = Modifier,
modifier: Modifier = Modifier,
isDarkBackground: Boolean,
btnScanStateInProgress: Boolean,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
onShopButtonClick: () -> Unit,
) {
val darkColorBackground = Color(0xFF26292E)
val darkColorButton = Color(0xFF080C10)
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = modifier
modifier = modifier,
) {
Button(
ProgressButton(
modifier = Modifier
.weight(1f)
.height(48.dp),
inProgress = btnScanStateInProgress,
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
contentColor = if (isDarkBackground) Color.White else darkColorButton,
onClick = onScanButtonClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
contentColor = if (isDarkBackground) Color.White else darkColorButton
)
) {
Text(
text = stringResource(id = R.string.home_button_scan),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center,
maxLines = 1
)
}
progressContent = {
CircularProgressIndicator(
modifier = Modifier.size(30.dp),
color = if (isDarkBackground) Color.White else darkColorBackground,
strokeWidth = 3.dp,
)
},
content = {
Text(
text = stringResource(id = R.string.home_button_scan),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center,
maxLines = 1,
)
},
)
SpacerS8()
Button(
modifier = Modifier
.weight(1f)
.height(48.dp),
onClick = onShopButtonClick,
enabled = !btnScanStateInProgress,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
contentColor = if (isDarkBackground) darkColorButton else Color.White
)
contentColor = if (isDarkBackground) darkColorButton else Color.White,
),
) {
Text(
text = stringResource(id = R.string.home_button_order),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center,
maxLines = 1
maxLines = 1,
)
}
}
}
@Composable
fun ProgressButton(
modifier: Modifier,
backgroundColor: Color,
contentColor: Color,
onClick: () -> Unit,
inProgress: Boolean,
progressContent: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
Button(
modifier = modifier,
onClick = onClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = backgroundColor,
contentColor = contentColor,
),
) {
if (progressContent != null && inProgress) {
progressContent()
} else {
content()
}
}
}

View file

@ -6,6 +6,7 @@ import org.rekotlin.Action
sealed class HomeAction : Action {
// from ui
object ReadCard : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data class GoToShop(val userCountryCode: String?) : HomeAction()
// internal
@ -14,5 +15,4 @@ sealed class HomeAction : Action {
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}

View file

@ -1,16 +1,20 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.common.TapWorkarounds.isSaltPayVisa
import com.tangem.common.core.TangemError
import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.post
import com.tangem.tap.common.postUi
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -20,13 +24,21 @@ import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.send.redux.states.ButtonState
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.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class HomeMiddleware {
@ -37,98 +49,129 @@ class HomeMiddleware {
}
}
private val homeMiddleware: Middleware<AppState> = { _, _ ->
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUi(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> {
handleReadCard()
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
}
is HomeAction.GoToShop -> {
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE ->
store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param),
)
}
}
handleHomeAction(state, action, dispatch)
next(action)
}
}
}
private fun handleReadCard() {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(
GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
// TODO: SaltPay: temporary excluding backup process for Visa cards
if (scanResponse.card.isSaltPayVisa) {
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
navigateTo(AppScreen.Wallet, null)
}
}
return@ScanCard
}
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo)
} else {
scope.launch {
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
}
store.onCardScanned(scanResponse)
}
}
},
onFailure = {
changeButtonState(ButtonState.ENABLED)
},
),
)
private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = ::onScanSuccess,
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
}
is HomeAction.GoToShop -> {
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param),
)
}
}
}
private fun onScanSuccess(scanResponse: ScanResponse) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(HomeAction.ScanInProgress(false))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.isSaltPay()) {
scope.launch {
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse)
delay(500)
withMainContext {
when (result) {
is Result.Success -> {
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.Update)
// store.dispatch(OnboardingSaltPayAction.Init.DiscardBackupSteps)
navigateTo(AppScreen.OnboardingWallet)
} else {
navigateTo(AppScreen.Wallet)
withIOContext { store.onCardScanned(scanResponse) }
}
}
is Result.Failure -> {
changeButtonState(ButtonState.ENABLED)
when (val error = result.error) {
is SaltPayRegistrationError -> {
val dialog = when (error) {
is SaltPayRegistrationError.NoGas -> SaltPayDialog.NoFundsForActivation
else -> SaltPayDialog.RegistrationError(error)
}
store.dispatchDialogShow(dialog)
}
else -> {
store.dispatchDebugErrorNotification(error.localizedMessage ?: "SaltPay: Unknown error")
}
}
}
}
}
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen)
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
scope.launch { store.onCardScanned(scanResponse) }
}
}
}
private fun onScanFailure(error: TangemError) {
store.dispatch(HomeAction.ScanInProgress(false))
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
post(DELAY_SDK_DIALOG_CLOSE) {
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))
}

View file

@ -15,6 +15,9 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
var state = state.homeState
when (action) {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.ShouldScanCardOnResume -> {
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
}

View file

@ -2,9 +2,15 @@ package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
data class HomeState(
val shouldScanCardOnResume: Boolean = false,
val scanInProgress: Boolean = false,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
) : StateType
) : StateType {
val btnScanStateInProgress: Boolean
get() = btnScanState.progressState == ProgressState.Loading
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.preferencesStorage
@ -16,19 +15,14 @@ class OnboardingHelper {
fun isOnboardingCase(response: ScanResponse): Boolean {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
return when {
response.card.isSaltPay -> {
response.card.backupStatus?.isActive != true
}
response.productType == ProductType.Twins -> {
response.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
} else {
cardInfoStorage.activationIsStarted(response.card.cardId)
}
}
response.card.hasWallets() -> {
cardInfoStorage.activationIsStarted(response.card.cardId)
}
response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId)
else -> true
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.successOr
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPayStep
/**
[REDACTED_AUTHOR]
*/
class OnboardingSaltPayHelper {
companion object {
suspend fun isOnboardingCase(scanResponse: ScanResponse): Result<Boolean> {
return try {
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val registrationResponseItem = manager.checkRegistration().successOr { return it }
val registrationStep = registrationResponseItem.toSaltPayStep()
manager.checkHasGas().successOr { return it }
val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished
val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false
Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase)
} catch (ex: Exception) {
Result.Failure(ex)
} catch (error: SaltPayRegistrationError){
Result.Failure(error)
}
}
}
}

View file

@ -49,7 +49,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
startPostponedEnterTransition()
binding.toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
@ -207,10 +207,10 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
}

View file

@ -116,10 +116,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
}

View file

@ -79,7 +79,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget =
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer)
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
@ -212,7 +212,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
chbUnderstand.hide()
pbBinding.pbState.show()
binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition()
binding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
@ -383,10 +383,10 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}

View file

@ -5,12 +5,14 @@ import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object StartSaltPayCardActivation: OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
object Done : OnboardingWalletAction()
object FinishOnboarding : OnboardingWalletAction()
data class ProceedBackup(val isSaltPay: Boolean = false) : OnboardingWalletAction()
object ResumeBackup : OnboardingWalletAction()
object LoadArtwork : OnboardingWalletAction()
class SetArtworkUrl(val artworkUrl: String?) : OnboardingWalletAction()
@ -21,7 +23,7 @@ sealed class OnboardingWalletAction : Action {
sealed class BackupAction : Action {
object DetermineBackupStep : BackupAction()
data class IntroduceBackup(val isSaltPay: Boolean = false) : BackupAction()
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object DismissBackup : BackupAction()
@ -53,15 +55,13 @@ sealed class BackupAction : Action {
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
object OnAccessCodeDialogClosed : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
object PrepareToWritePrimaryCard : BackupAction()
object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
object FinishBackup : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()
}

View file

@ -4,9 +4,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.extensions.ifNotNull
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
@ -19,6 +19,8 @@ import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
@ -28,6 +30,7 @@ import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class OnboardingWalletMiddleware {
@ -39,35 +42,57 @@ class OnboardingWalletMiddleware {
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleWalletAction(action)
handleWalletAction(action, state, dispatch)
next(action)
}
}
}
private fun handleWalletAction(action: Action) {
private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) {
if (action !is OnboardingWalletAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
val walletState = store.state.onboardingWalletState
val onboardingWalletState = store.state.onboardingWalletState
when (action) {
OnboardingWalletAction.Init -> {
val action = when {
// if (walletState.onboardingSaltPayState?.isTest == true) {
// store.dispatch(BackupAction.FinishBackup)
// return
// }
when {
card == null -> {
OnboardingWalletAction.ProceedBackup()
// StartForUnfinishedBackup -> navigateTo(OnboardingWallet) -> OnboardingWalletAction.Init
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> {
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.hasWallets() && card.backupStatus?.isActive == true -> {
if (scanResponse.isSaltPay()) {
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
}
store.dispatch(BackupAction.FinishBackup)
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
}
}
}
is OnboardingWalletAction.StartSaltPayCardActivation -> {
// we need to do this because the standard activation happens in the OnboardingWalletAction.CreateWallet
// step which is omitted for SaltPay cards
if (onboardingWalletState.isSaltPay) {
ifNotNull(onboardingManager, scanResponse?.card?.cardId) { manager, cardId ->
manager.activationStarted(cardId)
}
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup ->
OnboardingWalletAction.ProceedBackup(card.isSaltPay)
card.hasWallets() && card.backupStatus?.isActive == true ->
BackupAction.FinishBackup
else -> OnboardingWalletAction.GetToCreateWalletStep
}
store.dispatch(action)
}
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
@ -88,25 +113,24 @@ private fun handleWalletAction(action: Action) {
when (result) {
is CompletionResult.Success -> {
//here we must use updated scanResponse after createWallet & derivation
val updatedResponse =
globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard
)
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, result.data.card),
BlockchainNetwork(Blockchain.Ethereum, result.data.card)
BlockchainNetwork(Blockchain.Ethereum, result.data.card),
)
store.dispatchOnMain(
store.dispatch(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks,
card = result.data.card,
),
)
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingWalletAction.ProceedBackup())
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> {
// do nothing
@ -118,6 +142,7 @@ private fun handleWalletAction(action: Action) {
OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
//TODO: SaltPay: scanResponse
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
@ -128,15 +153,15 @@ private fun handleWalletAction(action: Action) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
is OnboardingWalletAction.ProceedBackup -> {
is OnboardingWalletAction.ResumeBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> {
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
walletState.backupState.backupStep == BackupStep.Finished
if (onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
) {
BackupAction.IntroduceBackup(action.isSaltPay)
BackupAction.IntroduceBackup
} else {
null
}
@ -145,7 +170,7 @@ private fun handleWalletAction(action: Action) {
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> {
when (walletState.backupState.backupStep) {
when (onboardingWalletState.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
@ -167,7 +192,7 @@ private fun updateScanResponseAfterBackup(
val cardsCount = backupState.backupCardsNumber
scanResponse.card.copy(
backupStatus = Card.BackupStatus.Active(cardCount = cardsCount),
isAccessCodeSet = true
isAccessCodeSet = true,
)
} else {
scanResponse.card
@ -188,13 +213,11 @@ class BackupMiddleware {
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
val globalState = appState()?.globalState ?: return
val onboardingWalletState = appState()?.onboardingWalletState ?: return
val backupState = store.state.onboardingWalletState.backupState
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val backupState = onboardingWalletState.backupState
val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
val card = scanResponse?.card
when (action) {
@ -221,8 +244,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
is BackupAction.AddBackupCard -> {
backupService.skipCompatibilityChecks = true
tangemSdk.config.filter.cardIdFilter =
CardFilter.Companion.ItemFilter.Allow(TapWorkarounds.saltPayTangemCardIds.toSet())
tangemSdk.config.filter.cardIdFilter = CardFilter.Companion.CardIdFilter.Allow(
items = SaltPayWorkaround.tangemWalletCardIds.toSet(),
ranges = SaltPayWorkaround.tangemWalletCardIdRanges,
)
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
@ -237,20 +262,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
if (backupService.addedBackupCardsCount == 2) {
if (backupService.addedBackupCardsCount == backupState.maxBackupCards) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
} else {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
}
is BackupAction.CheckAccessCode -> {
if (action.accessCode.length < 4) {
store.dispatch(
BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
)
)
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
} else {
store.dispatch(BackupAction.SetAccessCodeError(null))
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
@ -262,20 +282,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(
BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
)
)
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(1)
)
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
}
is CompletionResult.Failure -> {
}
@ -287,15 +301,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
when (result) {
is CompletionResult.Success -> {
if (backupService.currentState == BackupService.State.Finished) {
initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState)
store.dispatchOnMain(BackupAction.FinishBackup)
} else {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
)
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
}
}
is CompletionResult.Failure -> {
}
}
}
@ -321,13 +333,31 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeBackup -> {
store.dispatch(GlobalAction.Onboarding.Start(null, true))
store.dispatch(OnboardingWalletAction.ProceedBackup())
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
is BackupAction.ResumeFoundUnfinishedBackup -> {
// val isSaltPay = backupService.primaryBatchId?.let { SaltPayWorkaround.isVisaBatchId(it) } ?: false
// store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(isSaltPay))
// store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
store.dispatch(BackupAction.FinishBackup)
}
}
}
/**
* SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action
*/
private fun initSaltPayOnBackupFinishedIfNeeded(
scanResponse: ScanResponse?,
onboardingWalletState: OnboardingWalletState,
) {
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
//TODO: SaltPay: scanResponse
if (scanResponse == null) throw IllegalArgumentException()
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatchOnMain(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
store.dispatchOnMain(OnboardingSaltPayAction.Update)
}
}

View file

@ -3,12 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayReducer
import org.rekotlin.Action
class OnboardingWalletReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingWalletState =
internalReduce(action, state)
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
}
}
@ -16,13 +17,17 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
val state = appState.onboardingWalletState
return when (action) {
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
is GlobalAction.Onboarding.Start -> OnboardingWalletState(
backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen)
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state)
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay))
is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.GetToCreateWalletStep ->
OnboardingWalletState(step = OnboardingWalletStep.CreateWallet)
is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
step = OnboardingWalletStep.SaltPay,
)
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetArtworkUrl -> {
state.copy(cardArtworkUrl = action.artworkUrl)
}
@ -31,18 +36,45 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
}
}
private class ReducerForGlobalAction {
companion object {
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState = when (action) {
is GlobalAction.Onboarding.Start -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = if (action.scanResponse.isSaltPay()) 1 else 2,
canSkipBackup = if (action.scanResponse.isSaltPay()) false else action.canSkipBackup,
),
isSaltPay = action.scanResponse.isSaltPay()
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = if (action.isSaltPayBackup) 1 else 2,
canSkipBackup = false,
),
isSaltPay = action.isSaltPayBackup,
)
}
else -> state
}
}
}
class BackupReducer {
private class BackupReducer {
companion object {
fun reduce(
action: BackupAction, state: BackupState,
action: BackupAction,
state: BackupState,
isSaltPay: Boolean,
): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup,
isSaltPay = action.isSaltPay,
maxBackupCards = if (isSaltPay) 1 else 2,
canSkipBackup = !isSaltPay,
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {
@ -51,9 +83,6 @@ class BackupReducer {
BackupAction.AddBackupCard.Success -> {
state.copy(backupCardsNumber = state.backupCardsNumber + 1)
}
BackupAction.FinishAddingBackupCards -> {
state
}
BackupAction.ShowAccessCodeInfoScreen -> {
state.copy(backupStep = BackupStep.SetAccessCode)
}
@ -63,13 +92,11 @@ class BackupReducer {
is BackupAction.SaveFirstAccessCode -> {
state.copy(
backupStep = BackupStep.ReenterAccessCode,
accessCode = action.accessCode
accessCode = action.accessCode,
)
}
is BackupAction.SetAccessCodeError -> {
state.copy(
accessCodeError = action.error
)
state.copy(accessCodeError = action.error)
}
is BackupAction.PrepareToWritePrimaryCard -> {
if (state.primaryCardId == null) {
@ -77,12 +104,10 @@ class BackupReducer {
primaryCardId = backupService.primaryCardId,
backupCardIds = backupService.backupCardIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WritePrimaryCard
backupStep = BackupStep.WritePrimaryCard,
)
} else {
state.copy(
backupStep = BackupStep.WritePrimaryCard
)
state.copy(backupStep = BackupStep.WritePrimaryCard)
}
}
is BackupAction.PrepareToWriteBackupCard -> {
@ -91,43 +116,21 @@ class BackupReducer {
primaryCardId = backupService.primaryCardId,
backupCardIds = backupService.backupCardIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
backupStep = BackupStep.WriteBackupCard(action.cardNumber),
)
} else {
state.copy(
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
)
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
}
is BackupAction.FinishBackup -> {
state.copy(
backupStep = BackupStep.Finished
)
state.copy(backupStep = BackupStep.Finished)
}
BackupAction.OnAccessCodeDialogClosed -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.DiscardBackup -> BackupState()
BackupAction.WritePrimaryCard -> state
is BackupAction.WriteBackupCard -> state
is BackupAction.SaveAccessCodeConfirmation -> state
BackupAction.ScanPrimaryCard -> state
BackupAction.ShowEnterAccessCodeScreen -> state
BackupAction.ShowReenterAccessCodeScreen -> state
BackupAction.AddBackupCard -> state
BackupAction.DismissBackup -> state
is BackupAction.LoadBackupCardArtwork -> state
is BackupAction.CheckAccessCode -> state
BackupAction.DetermineBackupStep -> state
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.ResumeBackup -> state
BackupAction.DiscardSavedBackup -> state
BackupAction.StartBackup -> state
else -> state
}
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
import org.rekotlin.StateType
/**
@ -10,15 +12,20 @@ import org.rekotlin.StateType
data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val backupState: BackupState = BackupState(),
val onboardingSaltPayState: OnboardingSaltPayState? = null,
val isSaltPay: Boolean = false,
val cardArtworkUrl: String? = null,
val showConfetti: Boolean = false,
) : StateType {
fun getMaxProgress(): Int = when {
isSaltPay -> 12
else -> 6
}
fun getProgressStep(): Int {
return when {
step == OnboardingWalletStep.CreateWallet -> {
1
}
step == OnboardingWalletStep.CreateWallet -> 1
step == OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
BackupStep.InitBackup -> 2
@ -29,30 +36,41 @@ data class OnboardingWalletState(
BackupStep.ReenterAccessCode -> 4
BackupStep.SetAccessCode -> 4
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
BackupStep.Finished -> 6
BackupStep.Finished -> getMaxProgress()
}
}
step == OnboardingWalletStep.Done -> 6
step == OnboardingWalletStep.SaltPay && isSaltPay -> {
when (onboardingSaltPayState!!.step) {
SaltPayRegistrationStep.NoGas -> 1
SaltPayRegistrationStep.NeedPin -> 7
SaltPayRegistrationStep.CardRegistration -> 8
SaltPayRegistrationStep.Kyc -> 9
SaltPayRegistrationStep.KycStart -> 10
SaltPayRegistrationStep.KycWaiting -> 11
SaltPayRegistrationStep.Finished -> getMaxProgress()
}
}
step == OnboardingWalletStep.Done -> getMaxProgress()
else -> 1
}
}
}
enum class OnboardingWalletStep {
None, CreateWallet, Backup, Done
None, CreateWallet, Backup, SaltPay, Done
}
data class BackupState(
val primaryCardId: String? = null,
val backupCardsNumber: Int = 0,
val backupCardIds: List<CardId> = emptyList(),
@Transient
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep = BackupStep.InitBackup,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isSaltPay: Boolean = false,
)
enum class AccessCodeError {
@ -73,7 +91,7 @@ sealed class BackupStep {
object Finished : BackupStep()
}
sealed class BackupDialog: StateDialog {
sealed class BackupDialog : StateDialog {
object AddMoreBackupCards : StateDialog
object BackupInProgress : StateDialog
object UnfinishedBackupFound : StateDialog

View file

@ -0,0 +1,164 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.successOr
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stub
import com.tangem.tap.domain.getFirstToken
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.math.BigInteger
import java.util.concurrent.atomic.AtomicLong
/**
[REDACTED_AUTHOR]
*/
class GnosisRegistrator(
val walletManager: EthereumWalletManager,
) {
private val walletAddress = walletManager.wallet.address
private val token = walletManager.wallet.getFirstToken().guard {
throw NullPointerException("WalletManager for GnosisRegistrator must contain token")
}
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2"
Blockchain.SaltPayTestnet -> "0x710BF23486b549836509a08c184cE0188830f197"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
private val atomicNonce = AtomicLong()
suspend fun checkHasGas(): Result<Boolean> {
val wallet = walletManager.safeUpdate().successOr {
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
val hasGas = wallet.amounts[AmountType.Coin]?.value?.isPositive() ?: false
return Result.Success(hasGas)
}
suspend fun prepareBeforeMakingTxs(): Result<Unit> {
walletManager.safeUpdate().successOr {
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
atomicNonce.set(walletManager.txCount)
return Result.Success(Unit)
}
suspend fun sendTransactions(transactions: List<SignedEthereumTransaction>): Result<List<String>> {
val results = transactions.map {
coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } }
}.awaitAll()
val errors = results.filterIsInstance<SimpleResult.Failure>()
return if (errors.isEmpty()) {
Result.Success(listOf())
} else {
Result.Failure(errors[0].error)
}
}
suspend fun makeSetSpendLimitTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
val limitAmount = Amount(walletManager.cardTokens.first(), value)
val feeAmount = walletManager.getFeeToSetSpendLimit(otpProcessorContractAddress, limitAmount)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetSpendLimitToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
amount = limitAmount,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToSetSpendLimit,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setSpendLimit' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeInitOtpTx(rootOTP: ByteArray, rootOTPCounter: Int): Result<CompiledEthereumTransaction> {
val feeAmount = walletManager.getFeeToInitOTP(otpProcessorContractAddress, rootOTP, rootOTPCounter)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildInitOTPToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
otp = rootOTP,
otpCounter = rootOTPCounter,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToInitOTP,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'initOtp' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeSetWalletTx(): Result<CompiledEthereumTransaction> {
val feeAmount = walletManager.getFeeToSetWallet(otpProcessorContractAddress)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetWalletToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToSetWallet,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setWallet' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeApprovalTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
val approveAmount = Amount(token, value)
val feeAmount = walletManager.getFeeToApprove(approveAmount, otpProcessorContractAddress)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign(
transactionData = walletManager.createTransaction(
amount = approveAmount,
fee = feeAmount,
destination = otpProcessorContractAddress,
),
nonce = atomicNonce.getAndIncrement().toBigInteger(),
gasLimit = walletManager.gasLimitToApprove,
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'approval' transaction"))
return Result.Success(compiledEthereumTransaction)
}
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
return when (this) {
is Result.Success -> {
if (this.data.size != 3) Result.Failure(BlockchainSdkError.FailedToLoadFee)
else Result.Success(this.data[1])
}
is Result.Failure -> this
}
}
private fun Long?.toBigInteger(): BigInteger? = this?.let { BigInteger.valueOf(it) }
private fun Long.toBigInteger(): BigInteger = BigInteger.valueOf(this)
companion object {
fun stub(): GnosisRegistrator {
return GnosisRegistrator(WalletManager.stub())
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.common.Filter
/**
[REDACTED_AUTHOR]
*/
abstract class PinFilter : Filter<String>
class AllSymbolsTheSameFilter : PinFilter() {
override fun filter(value: String): Boolean {
if (value.isEmpty()) return false
val array = value.toCharArray()
val firstSymbol = array[0]
return array.all { it == firstSymbol }
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.tap.common.zendesk.ZendeskConfig
/**
[REDACTED_AUTHOR]
*/
data class SaltPayConfig(
val zendesk: ZendeskConfig,
val kycProvider: KYCProvider,
) {
companion object {
fun stub(): SaltPayConfig {
return SaltPayConfig(
zendesk = ZendeskConfig("", "", "", "", ""),
kycProvider = KYCProvider("", "", "", ""),
)
}
}
}
data class KYCProvider(
val baseUrl: String,
val externalIdParameterKey: String,
val sidParameterKey: String,
val sidValue: String,
)

View file

@ -0,0 +1,156 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import android.net.Uri
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.successOr
import com.tangem.network.api.paymentology.AttestationResponse
import com.tangem.network.api.paymentology.PaymentologyApiService
import com.tangem.network.api.paymentology.RegisterKYCRequest
import com.tangem.network.api.paymentology.RegisterWalletRequest
import com.tangem.network.api.paymentology.RegisterWalletResponse
import com.tangem.network.api.paymentology.RegistrationResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.tap.domain.tokens.UserWalletId
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
import com.tangem.tap.persistence.SaltPayRegistrationStorage
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class SaltPayRegistrationManager(
val cardId: String,
val cardPublicKey: ByteArray,
val walletPublicKey: ByteArray,
private val kycProvider: KYCProvider,
private val paymentologyService: PaymentologyApiService,
private val gnosisRegistrator: GnosisRegistrator,
private val registrationStorage: SaltPayRegistrationStorage,
) {
val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider)
private val approvalValue: BigDecimal = BigDecimal.ONE
private val spendLimitValue: BigDecimal = BigDecimal("100")
fun transactionIsSent(): Boolean {
return registrationStorage.data.transactionsSent
}
suspend fun checkHasGas(): Result<Unit> {
return when (val hasGasResult = gnosisRegistrator.checkHasGas()) {
is com.tangem.blockchain.extensions.Result.Success -> if (hasGasResult.data) {
Result.Success(Unit)
} else {
Result.Failure(SaltPayRegistrationError.NoGas)
}
is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayRegistrationError.NoGas)
}
}
suspend fun registerKYC(): Result<Unit> {
return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) {
is Result.Success -> Result.Success(Unit)
is Result.Failure -> result
}
}
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it }
return try {
if (!registrationResponse.success || registrationResponse.results.isEmpty()) {
throw SaltPayRegistrationError.EmptyResponse
}
Result.Success(registrationResponse.results[0])
} catch (ex: SaltPayRegistrationError) {
Result.Failure(ex)
}
}
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey)
}
suspend fun sendTransactions(
signedTransactions: List<SignedEthereumTransaction>,
): com.tangem.blockchain.extensions.Result<List<String>> {
return gnosisRegistrator.sendTransactions(signedTransactions)
}
suspend fun registerWallet(
attestResponse: AttestWalletKeyResponse,
pinCode: String,
): Result<RegisterWalletResponse> {
val request = RegisterWalletRequest(
cardId = cardId,
publicKey = cardPublicKey,
walletPublicKey = walletPublicKey,
walletSalt = attestResponse.salt,
walletSignature = attestResponse.walletSignature,
cardSalt = attestResponse.publicKeySalt ?: byteArrayOf(),
cardSignature = attestResponse.cardSignature ?: byteArrayOf(),
pin = pinCode,
)
val result = paymentologyService.registerWallet(request)
registrationStorage.data = registrationStorage.data.copy(
transactionsSent = result is Result.Success,
)
return result
}
fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask {
return SaltPayRegistrationTask(
gnosisRegistrator = gnosisRegistrator,
challenge = challenge,
walletPublicKey = walletPublicKey,
approvalValue = approvalValue,
spendLimitValue = spendLimitValue,
)
}
private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest(
cardId = cardId,
publicKey = cardPublicKey,
kycProvider = "UTORG",
kycRefId = kycUrlProvider.kycRefId,
)
companion object {
fun stub(): SaltPayRegistrationManager = SaltPayRegistrationManager(
kycProvider = SaltPayConfig.stub().kycProvider,
paymentologyService = PaymentologyApiService.stub(),
gnosisRegistrator = GnosisRegistrator.stub(),
registrationStorage = SaltPayRegistrationStorage.stub(),
cardId = "",
cardPublicKey = byteArrayOf(),
walletPublicKey = byteArrayOf(),
)
}
}
class KYCUrlProvider(
walletPublicKey: ByteArray,
kycProvider: KYCProvider,
) {
val kycRefId = UserWalletId(walletPublicKey).stringValue
val requestUrl: String = makeWebRequestUrl(kycProvider)
val doneUrl = "https://success.tangem.com/"
private fun makeWebRequestUrl(kycProvider: KYCProvider): String {
val baseUri = Uri.parse(kycProvider.baseUrl)
return Uri.Builder()
.scheme(baseUri.scheme)
.authority(baseUri.authority)
.encodedPath(baseUri.path)
.appendQueryParameter(kycProvider.sidParameterKey, kycProvider.sidValue)
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
.build().toString()
}
}

View file

@ -0,0 +1,170 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.CommandResponse
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.GenerateOTPResponse
import com.tangem.operations.attestation.AttestWalletKeyCommand
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.operations.sign.SignHashesCommand
import com.tangem.tap.scope
import kotlinx.coroutines.launch
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class RegistrationTaskResponse(
val signedTransactions: List<SignedEthereumTransaction>,
val attestResponse: AttestWalletKeyResponse,
) : CommandResponse
class SaltPayRegistrationTask(
private var gnosisRegistrator: GnosisRegistrator,
private var challenge: ByteArray,
private val walletPublicKey: ByteArray,
private val approvalValue: BigDecimal,
private val spendLimitValue: BigDecimal,
) : CardSessionRunnable<RegistrationTaskResponse> {
private var generateOTPResponse: GenerateOTPResponse? = null
private var attestWalletResponse: AttestWalletKeyResponse? = null
private var signedTransactions: List<SignedEthereumTransaction> = listOf()
override fun run(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
generateOTP(session, callback)
}
private fun generateOTP(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
GenerateOTPCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
generateOTPResponse = result.data
attestWallet(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun attestWallet(
session: CardSession,
callback: CompletionCallback<RegistrationTaskResponse>,
) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val walletPublicKey = card.wallets.firstOrNull()?.publicKey
if (walletPublicKey == null || !walletPublicKey.contentEquals(this.walletPublicKey)) {
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
return
}
val attestWalletCommand = AttestWalletKeyCommand(
publicKey = walletPublicKey,
challenge = challenge,
confirmationMode = AttestWalletKeyCommand.ConfirmationMode.Dynamic,
)
attestWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
attestWalletResponse = result.data
scope.launch { prepareTransactions(session, callback) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private suspend fun prepareTransactions(
session: CardSession,
callback: CompletionCallback<RegistrationTaskResponse>,
) {
val generateOTPResponse = this.generateOTPResponse.guard {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
return
}
gnosisRegistrator.prepareBeforeMakingTxs().successOr {
callback(CompletionResult.Failure(it.error))
return
}
val approvalTx = gnosisRegistrator.makeApprovalTx(approvalValue).successOr {
callback(CompletionResult.Failure(it.error))
return
}
val setWalletTx = gnosisRegistrator.makeSetWalletTx().successOr {
callback(CompletionResult.Failure(it.error))
return
}
val initOtpTx = gnosisRegistrator.makeInitOtpTx(
generateOTPResponse.rootOTP,
generateOTPResponse.rootOTPCounter,
).successOr {
callback(CompletionResult.Failure(it.error))
return
}
val setSpendLimitTx = gnosisRegistrator.makeSetSpendLimitTx(spendLimitValue).successOr {
callback(CompletionResult.Failure(it.error))
return
}
signTransactions(
transactions = listOf(
approvalTx,
setWalletTx,
initOtpTx,
setSpendLimitTx,
),
session = session,
callback = callback,
)
}
private fun signTransactions(
transactions: List<CompiledEthereumTransaction>,
session: CardSession,
callback: CompletionCallback<RegistrationTaskResponse>,
) {
val walletPublicKey = session.environment.card?.wallets?.firstOrNull()?.publicKey.guard {
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
return
}
val hashes = transactions.map { it.hash }.toTypedArray()
val signHashesCommand = SignHashesCommand(hashes, walletPublicKey)
signHashesCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
signedTransactions = transactions.zip(result.data.signatures).map {
SignedEthereumTransaction(it.first, it.second)
}
complete(callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun complete(callback: CompletionCallback<RegistrationTaskResponse>) {
if (signedTransactions.isEmpty() || attestWalletResponse == null) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
} else {
val result = RegistrationTaskResponse(signedTransactions, attestWalletResponse!!)
callback(CompletionResult.Success(result))
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingSaltPayAction : Action {
sealed class Init : OnboardingSaltPayAction() {
data class SetDependencies(
val registrationManager: SaltPayRegistrationManager,
val saltPayConfig: SaltPayConfig,
) : Init()
// only for tests
object DiscardBackupSteps : Init()
}
data class SetIsBusy(val isBusy: Boolean) : OnboardingSaltPayAction()
data class SetAccessCode(val accessCode: ByteArray?) : OnboardingSaltPayAction()
object Update : OnboardingSaltPayAction()
object RegisterCard : OnboardingSaltPayAction()
object KYCStart : OnboardingSaltPayAction()
object OnFinishKYC : OnboardingSaltPayAction()
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
data class SetPin(val pin: String) : OnboardingSaltPayAction()
// data class TryUpdateStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction()
data class SetStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction()
}

View file

@ -0,0 +1,288 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.Message
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.Filter
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.network.api.paymentology.RegistrationResponse
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.AllSymbolsTheSameFilter
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class OnboardingSaltPayMiddleware {
companion object {
val handler = onboardingSaltPayMiddleware
}
}
private val onboardingSaltPayMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleOnboardingSaltPayAction(action, state)
next(action)
}
}
}
private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> AppState?) {
if (DemoHelper.tryHandle(appState, anyAction)) return
val action = anyAction as? OnboardingSaltPayAction ?: return
appState() ?: return
fun getAppState(): AppState = appState()!!
fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
when (action) {
is OnboardingSaltPayAction.RegisterCard -> {
handleIsBusy = true
val state = getState()
scope.launch {
val saltPayManager = state.saltPayManager
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
onException(it.error)
return@launch
}
val pinCode = state.pinCode.guard {
onError(SaltPayRegistrationError.NeedPin)
return@launch
}
val attestationResponse = saltPayManager.requestAttestationChallenge().successOr {
onException(it.error)
return@launch
}
if (!attestationResponse.success) {
onError(SaltPayRegistrationError.CardNotFound)
return@launch
}
val registrationResponse = tangemSdkManager.runTaskAsync(
runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!),
cardId = saltPayManager.cardId,
initialMessage = Message(
header = null,
body = tangemSdkManager.getString(R.string.registration_task_alert_message),
),
).successOr {
onException(it.error as Exception)
return@launch
}
saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr {
onError(SaltPayRegistrationError.FailedToSendTx)
return@launch
}
saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr {
onException(it.error)
return@launch
}
withMainContext {
handleIsBusy = false
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.Kyc))
}
}
}
is OnboardingSaltPayAction.KYCStart -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycStart))
}
is OnboardingSaltPayAction.OnFinishKYC -> {
scope.launch {
registerKYCIfNeeded(getState()).successOr {
onException(it.error)
}
}
}
is OnboardingSaltPayAction.TrySetPin -> {
try {
assertPinValid(action.pin, getState().pinLength)
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.CardRegistration))
} catch (error: SaltPayRegistrationError) {
handleError = error
}
}
is OnboardingSaltPayAction.Update -> {
handleIsBusy = true
val state = getState()
scope.launch {
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
onException(it.error)
return@launch
}
registerKYCIfNeeded(state).successOr {
onException(it.error)
return@launch
}
val newStep = checkRegistration(state).successOr {
onException(it.error)
return@launch
}
withMainContext {
handleIsBusy = false
store.dispatch(OnboardingSaltPayAction.SetStep(newStep))
}
}
}
else -> {
/* do nothing, only reduce */
}
}
}
private var handleIsBusy: Boolean = false
set(value) {
field = value
store.dispatchOnMain(OnboardingSaltPayAction.SetIsBusy(value))
}
private suspend fun onError(error: SaltPayRegistrationError) {
withMainContext {
handleIsBusy = false
handleError = error
}
}
private suspend fun onException(error: Throwable) {
withMainContext {
handleIsBusy = false
handleException = error
}
}
private var handleException: Throwable = Throwable("Init")
set(value) {
field = value
when (value) {
is SaltPayRegistrationError -> handleError = value
else -> {
Timber.e(value)
when (value) {
is TangemSdkError -> {
// store.dispatchErrorNotification(TapError.CustomError(value.customMessage))
}
else -> {
store.dispatchErrorNotification(
TapError.CustomError(value.message ?: "Unknown SaltPay exception"),
)
}
}
}
}
}
private var handleError: SaltPayRegistrationError = SaltPayRegistrationError.Unknown
set(value) {
field = value
when (value) {
SaltPayRegistrationError.NoGas -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.NoGas))
}
else -> {
store.dispatchDialogShow(SaltPayDialog.RegistrationError(value))
}
}
}
private suspend fun checkGasIfNeeded(
saltPayManager: SaltPayRegistrationManager,
step: SaltPayRegistrationStep,
): Result<Unit> {
if (step == SaltPayRegistrationStep.KycStart ||
step == SaltPayRegistrationStep.KycWaiting ||
step == SaltPayRegistrationStep.Finished
) {
return Result.Success(Unit)
}
return saltPayManager.checkHasGas()
}
private suspend fun registerKYCIfNeeded(state: OnboardingSaltPayState): Result<Unit> {
if (state.step != SaltPayRegistrationStep.KycStart) {
return Result.Success(Unit)
}
withMainContext {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycWaiting))
}
return state.saltPayManager.registerKYC()
}
private suspend fun checkRegistration(
state: OnboardingSaltPayState,
): Result<SaltPayRegistrationStep> {
return try {
val registrationResponseItem = state.saltPayManager.checkRegistration().successOr { return it }
val step = registrationResponseItem.toSaltPayStep()
Result.Success(step)
} catch (ex: SaltPayRegistrationError) {
Result.Failure(ex)
}
}
@Throws(SaltPayRegistrationError::class)
fun RegistrationResponse.Item.toSaltPayStep(): SaltPayRegistrationStep {
return when {
passed != true -> throw SaltPayRegistrationError.CardNotPassed
disabledByAdmin == true -> throw SaltPayRegistrationError.CardDisabled
// go to success screen
active == true -> SaltPayRegistrationStep.Finished
// pinSet is false, go toPin screen
pinSet == false -> SaltPayRegistrationStep.NeedPin
// kycDate is set, go to kyc waiting screen
kycDate != null -> SaltPayRegistrationStep.KycWaiting
else -> SaltPayRegistrationStep.KycStart
}
}
@Throws(SaltPayRegistrationError::class)
private fun assertPinValid(pin: String, pinLength: Int) {
val array = pin.toCharArray()
if (array.size < pinLength) throw SaltPayRegistrationError.WeakPin
val filters: List<Filter<String>> = listOf(
AllSymbolsTheSameFilter(),
)
if (filters.any { it.filter(pin) }) {
throw SaltPayRegistrationError.WeakPin
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
class OnboardingSaltPayReducer {
companion object {
fun reduce(action: Action, state: OnboardingWalletState): OnboardingWalletState {
return internalReduce(action, state)
}
}
}
private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState {
val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.Init.SetDependencies) {
return onboardingWalletState.copy(
onboardingSaltPayState = OnboardingSaltPayState(
saltPayManager = action.registrationManager,
saltPayConfig = action.saltPayConfig,
),
)
}
val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState
val newState = when (action) {
OnboardingSaltPayAction.Init.DiscardBackupSteps -> {
return onboardingWalletState.copy(
step = OnboardingWalletStep.SaltPay,
backupState = onboardingWalletState.backupState.copy(
backupStep = BackupStep.Finished,
maxBackupCards = 1,
canSkipBackup = false,
),
onboardingSaltPayState = oldState.copy(
isTest = true,
step = SaltPayRegistrationStep.NeedPin,
),
)
}
is OnboardingSaltPayAction.SetAccessCode -> {
oldState.copy(
accessCode = action.accessCode,
)
}
is OnboardingSaltPayAction.SetPin -> {
oldState.copy(pinCode = action.pin)
}
is OnboardingSaltPayAction.SetStep -> {
if (action.newStep == null) {
oldState
} else {
oldState.copy(
step = action.newStep,
)
}
}
is OnboardingSaltPayAction.SetIsBusy -> {
oldState.copy(
isBusy = action.isBusy,
)
}
else -> oldState
// is OnboardingSaltPayAction.Init.PrepareAndSetDependencies -> oldState
// OnboardingSaltPayAction.OnCreate -> oldState
// is OnboardingSaltPayAction.OnFinishKYC -> oldState
// is OnboardingSaltPayAction.Register -> oldState
// is OnboardingSaltPayAction.TrySetPin -> oldState
// is OnboardingSaltPayAction.Update -> oldState
// is OnboardingSaltPayAction.TryUpdateStep -> oldState
}
return when (newState) {
oldState -> onboardingWalletState
else -> {
onboardingWalletState.copy(onboardingSaltPayState = newState)
}
}
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.paymentology.PaymentologyApiService
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.extensions.makeSaltPayWalletManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator
import com.tangem.tap.features.onboarding.products.wallet.saltPay.KYCProvider
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.persistence.SaltPayRegistrationStorage
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
data class OnboardingSaltPayState(
@Transient
val saltPayManager: SaltPayRegistrationManager = SaltPayRegistrationManager.stub(),
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
val pinCode: String? = null,
val accessCode: ByteArray? = null,
val step: SaltPayRegistrationStep = SaltPayRegistrationStep.NeedPin,
val saltPayCardArtworkUrl: String? = null,
val isBusy: Boolean = false,
val isTest: Boolean = false,
) {
val mainButtonState: WidgetState
get() = if (isBusy) ProgressState.Loading else ProgressState.Done
val pinLength: Int = 4
companion object {
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayRegistrationManager, SaltPayConfig> {
val globalState = store.state.globalState
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
throw NullPointerException("SaltPayConfig is not initialized")
}
val gnosisRegistrator = makeGnosisRegistrator(
scanResponse = scanResponse,
wmFactory = globalState.tapWalletManager.walletManagerFactory,
)
val registrationManager = makeSaltPayRegistrationManager(
scanResponse = scanResponse,
gnosisRegistrator = gnosisRegistrator,
paymentologyService = store.state.domainNetworks.paymentologyService,
registrationStorage = preferencesStorage.saltPayRegistrationStorage,
kycProvider = saltPayConfig.kycProvider,
)
return registrationManager to saltPayConfig
}
fun makeSaltPayRegistrationManager(
scanResponse: ScanResponse,
gnosisRegistrator: GnosisRegistrator,
paymentologyService: PaymentologyApiService,
registrationStorage: SaltPayRegistrationStorage,
kycProvider: KYCProvider,
): SaltPayRegistrationManager {
if (!scanResponse.isSaltPay()) {
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
}
val saltPaySingleWallet = scanResponse.card.wallets.firstOrNull().guard {
throw NullPointerException("SaltPay card must have one wallet at least")
}
return SaltPayRegistrationManager(
cardId = scanResponse.card.cardId,
cardPublicKey = scanResponse.card.cardPublicKey,
walletPublicKey = saltPaySingleWallet.publicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
registrationStorage = registrationStorage,
)
}
fun makeGnosisRegistrator(
scanResponse: ScanResponse,
wmFactory: WalletManagerFactory,
): GnosisRegistrator {
return GnosisRegistrator(
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
)
}
}
}
enum class SaltPayRegistrationStep {
NoGas,
NeedPin,
CardRegistration,
Kyc,
KycStart,
KycWaiting,
Finished;
}

View file

@ -1,25 +1,40 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.button.MaterialButton
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.extensions.guard
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.compose.PinCodeWidget
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stop
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
@ -28,24 +43,31 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import org.rekotlin.StoreSubscriber
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: WalletCardsWidget
private val binding: FragmentOnboardingWalletBinding by viewBinding(
FragmentOnboardingWalletBinding::bind
)
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(
ViewOnboardingProgressBinding::bind)
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
private val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
private val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
private val saltPayView: SaltPayView = SaltPayView(this)
private lateinit var cardsWidget: WalletCardsWidget
private var accessCodeDialog: AccessCodeDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -74,19 +96,19 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
TabLayoutMediator(
binding.tabLayoutBackupInfo,
binding.viewPagerBackupInfo
binding.viewPagerBackupInfo,
) { tab, position ->
//Some implementation
}.attach()
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
store.dispatch(OnboardingWalletAction.Init)
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
store.dispatch(OnboardingWalletAction.LoadArtwork)
addBackPressHandler(this)
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(OnboardingWalletAction.StartSaltPayCardActivation)
store.dispatch(OnboardingWalletAction.LoadArtwork)
}
override fun onStart() {
@ -108,23 +130,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
requireActivity().invalidateOptionsMenu()
with(binding) {
loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard)
loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard)
}
pbBinding.pbState.max = 6
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
when (state.step) {
OnboardingWalletStep.None -> {
}
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState)
OnboardingWalletStep.Done -> {
}
if (state.isSaltPay) {
saltPayView.newState(state)
} else {
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUrl, binding.imvSecondBackupCard)
handleOnboardingStep(state)
}
}
@ -136,6 +151,14 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
private fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
else -> {}
}
}
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
@ -149,12 +172,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
cardsWidget.toFolded(false) { startPostponedEnterTransition() }
}
private fun setBackupState(state: BackupState) {
private fun setBackupState(state: BackupState, isSaltPay: Boolean) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state)
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
@ -164,7 +186,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
private fun showBackupIntro(state: BackupState) = with(binding) {
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
@ -177,12 +199,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
with(layoutButtonsCommon) {
btnMainAction.text = getText(R.string.onboarding_button_backup_now)
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnAlternativeAction.show(state.canSkipBackup)
btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
btnAlternativeAction.show(!state.isSaltPay)
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
btnAlternativeAction.show(state.canSkipBackup && !isSaltPay)
}
startPostponedEnterTransition()
@ -217,7 +238,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
imvSecondBackupCard.show()
}
private fun showAddBackupCards(state: BackupState) = with(binding) {
private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) {
prepareBackupView()
accessCodeDialog?.dismiss()
@ -232,6 +253,15 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
layoutButtonsAddCards.btnAddCard.isEnabled = false
}
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
if (isSaltPay) {
saltPayView.showAddBackupCards(state)
return@with
}
when (state.backupCardsNumber) {
0 -> {
cardsWidget.toFan {
@ -258,10 +288,6 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
cardsWidget.getSecondBackupCardView().animate().alpha(1f).duration = 400
}
}
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
}
private fun showSetAccessCode() {
@ -307,7 +333,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
tvHeader.text = getText(R.string.onboarding_title_prepare_origin)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin)
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
@ -350,15 +376,14 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tvHeader.text =
getString(R.string.onboarding_title_backup_card_format, cardNumber)
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1])
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
)
layoutButtonsCommon.btnMainAction.text = getString(
R.string.onboarding_button_backup_card_format,
cardNumber
cardNumber,
)
layoutButtonsCommon.btnMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
@ -398,4 +423,192 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
override fun handleOnBackPressed() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
private class SaltPayView(
private val walletFragment: OnboardingWalletFragment,
) {
private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar }
private var progressButton: SaltPayProgressButton? = null
fun newState(state: OnboardingWalletState) {
handleCardArtworks(state)
when (state.step) {
OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState)
OnboardingWalletStep.Backup -> {
if (state.backupState.backupStep == BackupStep.Finished) {
// if backup finished -> switch OnboardingWalletStep to the SaltPay
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
} else {
// if not -> back to the standard backup process
walletFragment.handleOnboardingStep(state)
}
}
else -> walletFragment.handleOnboardingStep(state)
}
}
private fun handleCardArtworks(state: OnboardingWalletState) = with(walletFragment.binding) {
if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
// if saltPay url not loaded -> load from resource
val bitmap = BitmapFactory.decodeResource(walletFragment.resources, R.drawable.img_salt_pay_visa)
imvFrontCard.setImageBitmap(bitmap)
} else {
walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard)
}
walletFragment.loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
//TODO: at now we can hide the image only by changing alpha channel to 0, because
// the OnboardingWalletFragment and WalletCardsWidget manipulate it visibility through changing
// View.VISIBILITY states.
imvSecondBackupCard.alpha = 0f
}
fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) {
walletFragment.cardsWidget.getSecondBackupCardView().alpha = 0f
when (state.backupCardsNumber) {
0 -> {
walletFragment.cardsWidget.toFan {
walletFragment.cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).duration = 200
}
tvHeader.text = walletFragment.getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = walletFragment.getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = walletFragment.getText(R.string.onboarding_title_one_backup_card)
tvBody.text = walletFragment.getText(R.string.onboarding_subtitle_one_backup_card)
walletFragment.cardsWidget.toFan(false)
walletFragment.cardsWidget.getFirstBackupCardView().animate().alpha(1f).duration = 400
}
}
}
private fun setSaltPayStep(onboardingSaltPayState: OnboardingSaltPayState?) {
val state = onboardingSaltPayState.guard {
store.dispatchDebugErrorNotification("SaltPayView: setSaltPayStep: onboardingSaltPayState = null")
return
}
walletFragment.binding.onboardingWalletContainer.hide()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.show()
when (state.step) {
SaltPayRegistrationStep.NoGas -> handleNoGas()
SaltPayRegistrationStep.NeedPin -> handleNeedPin()
SaltPayRegistrationStep.CardRegistration -> handleCardRegistration()
SaltPayRegistrationStep.Kyc -> handleKyc()
SaltPayRegistrationStep.KycStart -> handleKycStart(state)
SaltPayRegistrationStep.KycWaiting -> handleKycWaiting()
SaltPayRegistrationStep.Finished -> handleFinished()
}
progressButton?.changeState(state.mainButtonState)
}
private fun handleNoGas() {
store.dispatchDialogShow(SaltPayDialog.NoFundsForActivation)
}
private fun handleNeedPin() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_pin)
showOnlyView(pinCode.root) {
progressButton = SaltPayProgressButton(pinCode.root)
progressButton?.mainView?.isEnabled = false
var enteredPinCode = ""
pinCode.flPinCodeContainer.setContent {
PinCodeWidget { code, isLastSymbolEntered ->
enteredPinCode = code
progressButton?.mainView?.isEnabled = isLastSymbolEntered
}
}
pinCode.btnSetPin.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.TrySetPin(enteredPinCode))
}
}
}
private fun handleCardRegistration() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_register_wallet)
showOnlyView(connectCard.root) {
progressButton = SaltPayProgressButton(connectCard.root)
connectCard.btnConnect.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.RegisterCard)
}
}
}
private fun handleKyc() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_start)
showOnlyView(verifyIdentity.root) {
progressButton = SaltPayProgressButton(verifyIdentity.root)
verifyIdentity.btnVerify.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.KYCStart)
}
}
}
private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_start)
showOnlyView(webvVerifyIdentity) {
progressButton = null
webvVerifyIdentity.configureSettings()
webvVerifyIdentity.webViewClient = UtorgWebViewClient(
successUrl = state.saltPayManager.kycUrlProvider.doneUrl,
onSuccess = {
webvVerifyIdentity.stop()
store.dispatch(OnboardingSaltPayAction.OnFinishKYC)
},
)
webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity())
webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl)
}
}
private fun handleKycWaiting() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_waiting)
showOnlyView(verifyIdentityInProgress.root) {
progressButton = SaltPayProgressButton(verifyIdentityInProgress.root)
verifyIdentityInProgress.btnOpenSupportChat.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
verifyIdentityInProgress.btnRefresh.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update)
}
}
}
private fun handleFinished() {
walletFragment.binding.onboardingRoot.beginDelayedTransition()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide()
walletFragment.binding.onboardingWalletContainer.show()
walletFragment.showSuccess()
}
private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) {
// onboardingSaltpayContainer.beginDelayedTransition()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.children
.filter { it.id != view.id }
.forEach { it.hide() }
view.show { onShowListener?.invoke() }
}
private class SaltPayProgressButton(
root: ViewGroup,
) : IndeterminateProgressButtonWidget(findButton(root), findProgress(root)) {
companion object {
fun findButton(view: ViewGroup): MaterialButton {
return findContainer(view).children.firstOrNull { it is MaterialButton } as MaterialButton
}
fun findProgress(view: ViewGroup): View {
return findContainer(view).children.firstOrNull { it is ProgressBar }!!
}
fun findContainer(view: ViewGroup): ViewGroup {
return view.children.first { it.id == R.id.btn_container } as ViewGroup
}
}
}
}
}

View file

@ -14,7 +14,7 @@ class ConfirmDiscardingBackupDialog {
setTitle(R.string.welcome_interrupted_backup_discard_title)
setMessage(R.string.welcome_interrupted_backup_discard_message)
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
store.dispatch(BackupAction.ResumeBackup)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
store.dispatch(BackupAction.DiscardSavedBackup)

View file

@ -16,7 +16,7 @@ class UnfinishedBackupFoundDialog {
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeBackup)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
store.dispatch(GlobalAction.HideDialog)

View file

@ -1,13 +0,0 @@
package com.tangem.tap.features.onboarding.service
//class OnboardingWalletService(
// override val fromScreen: AppScreen,
// override var scanResponse: ScanResponse,
// cardInfoStorage: UsedCardsPrefStorage,
// walletManagerFactory: WalletManagerFactory,
//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
//
// override fun navigateToScreen(): AppScreen {
// return AppScreen.OnboardingWallet
// }
//}

View file

@ -1,11 +1,37 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FabPosition
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@ -15,14 +41,23 @@ import androidx.compose.ui.unit.dp
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.*
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
import com.tangem.wallet.R

View file

@ -143,8 +143,8 @@ class MultiWalletMiddleware {
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
is WalletAction.MultiWallet.BackupWallet -> {
store.state.globalState.scanResponse?.let {
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
}
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {

View file

@ -42,7 +42,6 @@ import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
import org.rekotlin.StoreSubscriber
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
private lateinit var warningsAdapter: WarningMessagesAdapter
@ -89,7 +88,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
// addCustomActionOnCard()
addCustomActionOnCard()
}
private fun addCustomActionOnCard() {

View file

@ -15,6 +15,7 @@ class PreferencesStorage(applicationContext: Application) {
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
val saltPayRegistrationStorage: SaltPayRegistrationStorage
init {
incrementLaunchCounter()
@ -23,6 +24,7 @@ class PreferencesStorage(applicationContext: Application) {
usedCardsPrefStorage.migrate()
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
fiatCurrenciesPrefStorage.migrate()
saltPayRegistrationStorage = SaltPayRegistrationPrefStorage(applicationContext, MoshiJsonConverter.INSTANCE)
}
var chatFirstLaunchTime: Long?

View file

@ -0,0 +1,82 @@
package com.tangem.tap.persistence
import android.content.Context
import androidx.core.content.edit
import at.favre.lib.armadillo.ArmadilloSharedPreferences
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
/**
[REDACTED_AUTHOR]
*/
interface SaltPayRegistrationStorage {
var data: SaltPayRegistratorData
fun reset()
companion object {
fun stub(): SaltPayRegistrationStorage = object : SaltPayRegistrationStorage {
override var data: SaltPayRegistratorData = SaltPayRegistratorData()
override fun reset() {}
}
}
}
data class SaltPayRegistratorData(
val transactionsSent: Boolean = false,
)
class SaltPayRegistrationPrefStorage(
context: Context,
private val converter: MoshiJsonConverter,
) : SaltPayRegistrationStorage {
private val storage: ArmadilloSharedPreferences = SecureStorage.createEncryptedSharedPreferences(
context = context,
storageName = "saltpay",
)
private var isFetching: Boolean = false
init {
fetch()
}
override var data: SaltPayRegistratorData = SaltPayRegistratorData()
set(value) {
field = value
if (!isFetching) save()
}
override fun reset() {
storage.edit(true) {
this.remove(REGISTRATION_DATA_KEY)
data = SaltPayRegistratorData()
}
}
private fun save() {
storage.edit(true) {
putString(REGISTRATION_DATA_KEY, converter.toJson(data))
}
}
private fun fetch(): SaltPayRegistratorData {
isFetching = true
val jsonData = storage.getString(REGISTRATION_DATA_KEY, null)
data = if (jsonData == null) {
SaltPayRegistratorData()
} else {
converter.fromJson<SaltPayRegistratorData>(jsonData) ?: SaltPayRegistratorData()
}
isFetching = false
return data
}
companion object {
private const val REGISTRATION_DATA_KEY = "saltpay_registration_data"
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="66dp"
android:height="65dp"
android:viewportWidth="66"
android:viewportHeight="65">
<path
android:pathData="M33.43,36.488L24.961,44.957L22.97,42.967L20.924,40.935L29.392,32.465C30.285,31.573 30.634,30.272 30.307,29.053C29.98,27.834 29.028,26.881 27.809,26.554C26.59,26.228 25.288,26.576 24.396,27.469L15.927,35.938L13.937,33.948C13.681,33.649 13.312,33.47 12.919,33.455C12.526,33.44 12.145,33.589 11.867,33.868C11.588,34.146 11.439,34.527 11.454,34.92C11.469,35.313 11.648,35.682 11.946,35.938L5.961,41.979C4.639,43.303 3.895,45.097 3.895,46.969C3.895,48.84 4.639,50.635 5.961,51.958L6.964,52.961L3.957,55.953C3.41,56.503 3.41,57.392 3.957,57.943L4.96,58.945L1.374,62.516C1.076,62.772 0.897,63.141 0.882,63.534C0.867,63.927 1.016,64.309 1.294,64.587C1.572,64.865 1.954,65.014 2.347,64.999C2.74,64.984 3.109,64.805 3.364,64.507L6.95,60.921L7.952,61.924C8.503,62.471 9.392,62.471 9.942,61.924L12.935,58.931L13.937,59.934H13.936C15.26,61.257 17.055,62 18.926,62C20.798,62 22.592,61.256 23.916,59.934L29.9,53.949V53.949C30.157,54.247 30.525,54.426 30.919,54.441C31.311,54.456 31.693,54.307 31.971,54.028C32.249,53.751 32.398,53.369 32.383,52.976C32.368,52.583 32.19,52.214 31.891,51.958L29.886,49.954L38.355,41.485V41.485C39.096,40.578 39.339,39.364 39.003,38.242C38.668,37.12 37.798,36.238 36.681,35.887C35.565,35.535 34.347,35.761 33.43,36.488L33.43,36.488ZM26.372,29.501C26.649,29.224 27.098,29.224 27.374,29.501C27.651,29.778 27.651,30.227 27.374,30.504L18.905,38.973L17.903,37.985L26.372,29.501ZM10.846,56.956L8.954,58.946L6.95,56.941L8.94,54.951L10.846,56.956ZM21.827,57.958V57.957C21.033,58.75 19.957,59.196 18.835,59.196C17.712,59.196 16.636,58.75 15.842,57.957L7.952,49.954C7.159,49.16 6.714,48.084 6.714,46.962C6.714,45.84 7.159,44.763 7.952,43.97L13.937,37.985L27.911,51.959L21.827,57.958ZM36.394,39.523L27.925,47.992L26.923,46.99L35.392,38.521V38.521C35.669,38.244 36.117,38.244 36.394,38.521C36.671,38.798 36.671,39.247 36.394,39.523V39.523Z"
android:fillColor="#919191"/>
<path
android:pathData="M65.485,0.41C64.934,-0.137 64.045,-0.137 63.494,0.41L56.014,7.891L55.011,6.889V6.889C54.461,6.342 53.572,6.342 53.021,6.889L49.789,10.122L48.786,9.119L48.787,9.12C47.463,7.796 45.668,7.053 43.797,7.053C41.925,7.053 40.131,7.796 38.807,9.12L31.312,16.615C30.752,16.135 29.918,16.167 29.396,16.688C28.875,17.21 28.843,18.045 29.322,18.605L30.325,19.607L46.274,35.586L47.277,36.588L47.276,36.588C47.836,37.067 48.671,37.035 49.193,36.513C49.714,35.992 49.746,35.158 49.266,34.597L56.761,27.103H56.762C58.084,25.779 58.828,23.984 58.828,22.113C58.828,20.241 58.084,18.446 56.762,17.123L55.759,16.121L58.991,12.889H58.992C59.539,12.338 59.539,11.449 58.992,10.898L58.004,9.882L65.47,2.415C65.74,2.152 65.892,1.792 65.895,1.416C65.897,1.04 65.75,0.678 65.484,0.411L65.485,0.41ZM54.771,25.098L47.276,32.593L33.302,18.619L40.797,11.124C41.604,10.354 42.675,9.924 43.79,9.924C44.904,9.924 45.976,10.354 46.782,11.124L54.772,19.113L54.771,19.113C55.564,19.906 56.01,20.983 56.01,22.105C56.01,23.227 55.564,24.304 54.771,25.097L54.771,25.098ZM53.769,14.116L51.779,12.126L54.023,9.881L56.014,11.872L53.769,14.116Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="142dp"
android:height="142dp"
android:viewportWidth="142"
android:viewportHeight="142">
<path
android:pathData="M71,0L71,0A71,71 0,0 1,142 71L142,71A71,71 0,0 1,71 142L71,142A71,71 0,0 1,0 71L0,71A71,71 0,0 1,71 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M71,142C110.21,142 142,110.21 142,71C142,31.79 110.21,0 71,0C31.79,0 0,31.79 0,71C0,110.21 31.79,142 71,142Z"
android:strokeAlpha="0.2"
android:fillColor="#FFB71B"
android:fillType="evenOdd"
android:fillAlpha="0.2"/>
<path
android:pathData="M70.66,117.31C96.43,117.31 117.31,96.43 117.31,70.66C117.31,44.89 96.43,24 70.66,24C44.89,24 24,44.89 24,70.66C24,96.43 44.89,117.31 70.66,117.31Z"
android:fillColor="#FFB71B"
android:fillType="evenOdd"/>
<path
android:pathData="M70.53,88.08C80.16,88.08 88.06,80.16 88.06,70.53C88.06,60.92 80.14,53 70.53,53C60.9,53 53,60.92 53,70.53C53,80.16 60.92,88.08 70.53,88.08ZM70.53,83.71C63.24,83.71 57.38,77.84 57.38,70.53C57.38,63.24 63.23,57.37 70.53,57.37C77.82,57.37 83.7,63.24 83.7,70.53C83.7,77.84 77.84,83.71 70.53,83.71ZM62.65,73.07H70.53C71.43,73.07 72.13,72.36 72.13,71.46V61.13C72.13,60.24 71.43,59.54 70.53,59.54C69.63,59.54 68.92,60.24 68.92,61.13V69.87H62.65C61.75,69.87 61.05,70.56 61.05,71.46C61.05,72.36 61.75,73.07 62.65,73.07Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="67dp"
android:height="66dp"
android:viewportWidth="67"
android:viewportHeight="66">
<path
android:pathData="M11.5,9.75L55.5,9.75A6.75,6.75 0,0 1,62.25 16.5L62.25,49.5A6.75,6.75 0,0 1,55.5 56.25L11.5,56.25A6.75,6.75 0,0 1,4.75 49.5L4.75,16.5A6.75,6.75 0,0 1,11.5 9.75z"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#919191"/>
<path
android:pathData="M28.068,26.775C28.068,30.44 25.458,33.183 22.188,33.183C18.945,33.183 16.335,30.44 16.335,26.775C16.335,23.348 18.945,20.5 22.188,20.5C25.458,20.5 28.068,23.348 28.068,26.775Z"
android:fillColor="#919191"/>
<path
android:pathData="M54.515,23.875H39.67C38.905,23.875 38.352,23.295 38.352,22.557C38.352,21.845 38.905,21.291 39.67,21.291H54.515C55.253,21.291 55.833,21.845 55.833,22.557C55.833,23.295 55.253,23.875 54.515,23.875Z"
android:fillColor="#919191"/>
<path
android:pathData="M54.515,34.686H39.67C38.905,34.686 38.352,34.105 38.352,33.367C38.352,32.655 38.905,32.075 39.67,32.075H54.515C55.253,32.075 55.833,32.655 55.833,33.367C55.833,34.105 55.253,34.686 54.515,34.686Z"
android:fillColor="#919191"/>
<path
android:pathData="M32.894,45.496H11.51C10.745,45.496 10.402,44.995 10.402,44.31C10.402,41.752 14.173,35.055 22.188,35.055C30.204,35.055 34.001,41.752 34.001,44.31C34.001,44.995 33.658,45.496 32.894,45.496Z"
android:fillColor="#919191"/>
<path
android:pathData="M54.515,45.496H39.67C38.905,45.496 38.352,44.916 38.352,44.204C38.352,43.466 38.905,42.886 39.67,42.886H54.515C55.253,42.886 55.833,43.466 55.833,44.204C55.833,44.916 55.253,45.496 54.515,45.496Z"
android:fillColor="#919191"/>
</vector>

View file

@ -1,7 +1,15 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M4,3.9C4,3.403 4.421,3 4.941,3H19.059C19.579,3 20,3.403 20,3.9V8.4H4V3.9Z"/>
<path android:fillColor="#000000" android:pathData="M4,12H9.647V21H4.941C4.421,21 4,20.597 4,20.1V12Z"/>
<path android:fillColor="#000000" android:pathData="M20,12H14.353V21H19.059C19.579,21 20,20.597 20,20.1V12Z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="20dp"
android:viewportWidth="21"
android:viewportHeight="20">
<path
android:pathData="M3.833,3.25C3.833,2.836 4.184,2.5 4.618,2.5H16.382C16.815,2.5 17.167,2.836 17.167,3.25V7H3.833V3.25Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M3.833,10H8.539V17.5H4.618C4.184,17.5 3.833,17.164 3.833,16.75V10Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M17.167,10H12.461V17.5H16.382C16.815,17.5 17.167,17.164 17.167,16.75V10Z"
android:fillColor="#ffffff"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -28,16 +28,17 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/onboarding_main_container"
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/onboarding_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
@ -46,175 +47,188 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="236dp"
android:layout_height="236dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle"
android:backgroundTint="@color/lightGray0"
android:elevation="0dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@id/fl_cards_container"
app:layout_constraintEnd_toEndOf="@id/fl_cards_container"
app:layout_constraintStart_toStartOf="@id/fl_cards_container"
app:layout_constraintTop_toTopOf="@id/fl_cards_container" />
<FrameLayout
android:id="@+id/fl_cards_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="0dp"
android:clipChildren="false"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pb_state">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_second_backup_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_first_backup_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:transitionName="imv_front_card" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_success"
android:layout_width="142dp"
android:layout_height="142dp"
android:src="@drawable/ic_activation_success"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pb_state" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_header_body_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="72dp"
android:layout_marginEnd="32dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2">
</androidx.appcompat.widget.LinearLayoutCompat>
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
android:textAlignment="center"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.504"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2"
tools:text="Добавлена одна резервная карта" />
<TextView
android:id="@+id/tv_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/home_bottom_tv_body_margin_start"
android:layout_marginTop="@dimen/home_bottom_tv_body_margin_top"
android:layout_marginEnd="@dimen/home_bottom_tv_body_margin_end"
android:textAlignment="center"
android:textColor="#686868"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header"
tools:text="Body" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_backup_info"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:flow_verticalBias="0"
app:layout_constraintTop_toTopOf="@+id/guideline2" />
android:layout_height="match_parent"
android:clipChildren="false">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_backup_info"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_marginBottom="16dp"
android:background="@color/backgroundLightGray"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_buttons_common"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_pager_backup_info"
app:tabBackground="@drawable/selector_tabs_dots"
app:tabGravity="center"
app:tabIndicator="@null"
app:tabPaddingEnd="8dp"
app:tabPaddingStart="8dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="236dp"
android:layout_height="236dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle"
android:backgroundTint="@color/lightGray0"
android:elevation="0dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@id/fl_cards_container"
app:layout_constraintEnd_toEndOf="@id/fl_cards_container"
app:layout_constraintStart_toStartOf="@id/fl_cards_container"
app:layout_constraintTop_toTopOf="@id/fl_cards_container" />
<FrameLayout
android:id="@+id/fl_cards_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="0dp"
android:clipChildren="false"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_second_backup_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_first_backup_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:transitionName="imv_front_card" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_success"
android:layout_width="142dp"
android:layout_height="142dp"
android:src="@drawable/ic_activation_success"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_header_body_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="72dp"
android:layout_marginEnd="32dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2">
</androidx.appcompat.widget.LinearLayoutCompat>
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
android:textAlignment="center"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.504"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2"
tools:text="Добавлена одна резервная карта" />
<TextView
android:id="@+id/tv_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/home_bottom_tv_body_margin_start"
android:layout_marginTop="@dimen/home_bottom_tv_body_margin_top"
android:layout_marginEnd="@dimen/home_bottom_tv_body_margin_end"
android:textAlignment="center"
android:textColor="#686868"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header"
tools:text="Body" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_backup_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:flow_verticalBias="0"
app:layout_constraintTop_toTopOf="@+id/guideline2" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_backup_info"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_marginBottom="16dp"
android:background="@color/backgroundLightGray"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_buttons_common"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_pager_backup_info"
app:tabBackground="@drawable/selector_tabs_dots"
app:tabGravity="center"
app:tabIndicator="@null"
app:tabPaddingEnd="8dp"
app:tabPaddingStart="8dp" />
<include
android:id="@+id/layout_buttons_common"
layout="@layout/layout_onboarding_buttons_common"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
<include
android:id="@+id/layout_buttons_add_cards"
layout="@layout/layout_onboarding_buttons_add_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="@dimen/onboarding_wallet_top_guide_line_weight"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/layout_buttons_common"
layout="@layout/layout_onboarding_buttons_common"
android:id="@+id/onboarding_saltpay_container"
layout="@layout/layout_onboarding_saltpay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
android:layout_height="match_parent"
android:visibility="gone" />
<include
android:id="@+id/layout_buttons_add_cards"
layout="@layout/layout_onboarding_buttons_add_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="@dimen/onboarding_wallet_top_guide_line_weight"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
<include
android:id="@+id/v_confetti"

View file

@ -1,32 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_card"
style="@style/OnboardingButton"
android:backgroundTint="@color/gray_button"
android:textColor="@color/selector_btn_gray_text_color"
android:elevation="0dp"
style="@style/OnboardingButton.Alternative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
tools:text="+ Add backup card"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_continue"
app:layout_constraintStart_toStartOf="@id/btn_continue" />
app:layout_constraintStart_toStartOf="@id/btn_continue"
tools:text="+ Add backup card" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
tools:text="Main Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
tools:text="Main Action" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -18,13 +18,10 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_alternative_action"
style="@style/Widget.AppCompat.Button.Borderless"
style="@style/OnboardingButton.Borderless"
android:layout_width="204dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/home_bottom_btn_alternative_action_margin_bottom"
android:textAllCaps="false"
android:textColor="@color/tapButtonColorBlack"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/onboarding_saltpay_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/pin_code"
layout="@layout/layout_onboarding_saltpay_pin_code"
android:visibility="gone" />
<include
android:id="@+id/connect_card"
layout="@layout/layout_onboarding_saltpay_connect_card"
android:visibility="gone" />
<include
android:id="@+id/verify_identity"
layout="@layout/layout_onboarding_saltpay_verify_identity"
android:visibility="gone" />
<WebView
android:id="@+id/webv_verify_identity"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="gone" />
<include
android:id="@+id/verify_identity_in_progress"
layout="@layout/layout_onboarding_saltpay_verify_identity_in_progress"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_connecting_socket"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_connecting_socket"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:text="@string/onboarding_title_register_wallet"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_connecting_socket" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_register_wallet"
app:layout_constraintBottom_toTopOf="@+id/btn_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_connect"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_register_wallet"
app:icon="@drawable/ic_tangem"
app:iconGravity="textEnd" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="94dp"
android:text="@string/onboarding_title_pin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_pin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/fl_pin_code_container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_body" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_set_pin"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_pin" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_passport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_passport"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:text="@string/onboarding_title_kyc_start"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_passport" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_kyc_start"
app:layout_constraintBottom_toTopOf="@+id/btn_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_verify"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_kyc_start" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_in_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="113dp"
android:src="@drawable/ic_in_progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/onboarding_title_kyc_waiting"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_in_progress"
app:layout_constraintVertical_bias="0.26999998"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_kyc_waiting"
app:layout_constraintBottom_toTopOf="@+id/btn_open_support_chat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_open_support_chat"
style="@style/OnboardingButton.Alternative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
android:text="@string/onboarding_supplement_button_kyc_waiting"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_container"
app:layout_constraintStart_toStartOf="@id/btn_container" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_refresh"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_kyc_waiting" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:id="@+id/onboarding_wallet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

View file

@ -1,9 +1,10 @@
ext.versions = [
kotlin : '1.6.10',
build_gradle : '7.1.3',
tangem_card_sdk : 'develop-163',
tangem_blockchain_sdk: 'develop-121',
// tangem_blockchain_sdk: '0.0.1',
// tangem_card_sdk : 'develop-164',
tangem_card_sdk : '0.0.1',
// tangem_blockchain_sdk: 'develop-122',
tangem_blockchain_sdk: '0.0.1',
]
ext.environmentConfig = [