Updated on 2026-08-14
This commit is contained in:
parent
d0c4d92a8c
commit
5fea60f2a2
83 changed files with 2801 additions and 775 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ val logMiddleware: Middleware<AppState> = { dispatch, appState ->
|
|||
{ action ->
|
||||
if (LogConfig.storeAction) {
|
||||
Timber.d("Dispatch action: $action")
|
||||
// printOnboardingWalletState()
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
//}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/drawable/ic_connecting_socket.xml
Normal file
12
app/src/main/res/drawable/ic_connecting_socket.xml
Normal 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>
|
||||
22
app/src/main/res/drawable/ic_in_progress.xml
Normal file
22
app/src/main/res/drawable/ic_in_progress.xml
Normal 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>
|
||||
26
app/src/main/res/drawable/ic_passport.xml
Normal file
26
app/src/main/res/drawable/ic_passport.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
BIN
app/src/main/res/drawable/img_salt_pay_visa.png
Normal file
BIN
app/src/main/res/drawable/img_salt_pay_visa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
35
app/src/main/res/layout/layout_onboarding_saltpay.xml
Normal file
35
app/src/main/res/layout/layout_onboarding_saltpay.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue