diff --git a/app/build.gradle b/app/build.gradle index c37a4da509..f1fa3b08ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index 5031d9d835..fc496f1d9b 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -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) { 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) diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 4eb6f5e2ed..deacbaf05f 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt index 31c826f6be..ad7bbe794b 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt index 8ea4eceef2..07c82d1384 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt index 60345e1ab7..cba31fe77c 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 7a0488d1a5..d88560669e 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -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) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 311932f7c1..76144c6132 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -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 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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt b/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt deleted file mode 100644 index a7247ebe20..0000000000 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt +++ /dev/null @@ -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 { - - 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}" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt b/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt deleted file mode 100644 index 7f5221041e..0000000000 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt +++ /dev/null @@ -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 { - override fun convert(message: DomainError): String? = when (message) { - is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message) - else -> null - } -} - -private class AddCustomTokenConverter( - private val context: Context -) : ModuleMessageConverter { - - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt b/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt deleted file mode 100644 index 3afb79b219..0000000000 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt +++ /dev/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 { - override fun convert(message: DomainModuleMessage): String? { - return when (message) { - is DomainError -> DomainErrorConverter(context).convert(message) - else -> null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 6f20496105..af79cb81e1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 1337effab2..30b127fda1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -12,6 +12,7 @@ val logMiddleware: Middleware = { dispatch, appState -> { action -> if (LogConfig.storeAction) { Timber.d("Dispatch action: $action") + // printOnboardingWalletState() } nextDispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 4927405ccd..0b285f8da9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 449d47ae10..d1f9043ca2 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -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)) } diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt index b4eacfff4e..cc57977202 100644 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 34e58f0ba7..9162ce8ce2 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 62f367cb33..9d0ae5a366 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 6e119fc5e6..70c7ca73db 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -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?, - 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?, + 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?) { diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 438b257916..5bd5575906 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 175fb62544..ec95e13dc6 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -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, + scanResponse: ScanResponse, + blockchains: List, ): List { 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? { 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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt b/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt new file mode 100644 index 0000000000..858dc8b5d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt @@ -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 { + private val converter = MoshiJsonConverter( + MoshiJsonConverter.getTangemSdkAdapters() + internalAdapters(), + MoshiJsonConverter.getTangemSdkTypedAdapters(), + ) + + private fun internalAdapters(): List { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index cdef91880f..d18b590dfd 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -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) -> 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) -> 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, diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index e418215ed7..de30f616f6 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index fa96a81962..cc9ccef1b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index d2013dba98..ef4a0aa975 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 60affefd8b..b238ffe8f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -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) { }, ) } 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) { CardInfo.Issuer(state.cardInfo.issuer), ) - if (!state.card.isTangemTwins()) { + if (!state.card.isTangemTwins) { cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString())) } cardDetails.add( diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 27953640ea..487362e5c1 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -81,16 +81,12 @@ class HomeFragment : Fragment(), StoreSubscriber { 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()) - } + }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index 21adc5e516..a459a4d701 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -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, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index d49013db87..cb40186a87 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 1328156afa..c22bd6c043 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -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() - } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index bd208b2baf..5fcd71137a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 = { _, _ -> +private val homeMiddleware: Middleware = { 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)) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index f4a7a9d407..511aea3ddc 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 38b19013d8..9ef6ea2b57 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -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 \ No newline at end of file +) : StateType { + + val btnScanStateInProgress: Boolean + get() = btnScanState.progressState == ProgressState.Loading +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index cd6f7c5197..f2227fcc32 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt new file mode 100644 index 0000000000..8b56bbc65a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt @@ -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 { + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index b31dd02832..a81bc28e87 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -49,7 +49,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { 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() { 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) } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index e2b1e015c4..bb09b3d434 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -116,10 +116,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { 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() { chbUnderstand.hide() pbBinding.pbState.show() - binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition() + binding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() when (previousStep) { TwinCardsStep.None -> { @@ -383,10 +383,10 @@ class TwinsCardsFragment : BaseOnboardingFragment() { 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) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 8431644dbf..ce9a6b6c41 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 1c41c5e27a..39d354a8b2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -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 = { 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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 13b86b3e08..0f79ba3da0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -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 } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 05a307b5b9..c83c9220a0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -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 = emptyList(), + @Transient val backupCardsArtworks: Map = 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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt new file mode 100644 index 0000000000..5937e34621 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -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 { + 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 { + walletManager.safeUpdate().successOr { + return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) + } + atomicNonce.set(walletManager.txCount) + + return Result.Success(Unit) + } + + suspend fun sendTransactions(transactions: List): Result> { + val results = transactions.map { + coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } } + }.awaitAll() + + val errors = results.filterIsInstance() + return if (errors.isEmpty()) { + Result.Success(listOf()) + } else { + Result.Failure(errors[0].error) + } + } + + suspend fun makeSetSpendLimitTx(value: BigDecimal): Result { + 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 { + 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 { + 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 { + 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>.extractFeeAmount(): Result { + 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()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt new file mode 100644 index 0000000000..15a4137ec0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.common.Filter + +/** +[REDACTED_AUTHOR] + */ +abstract class PinFilter : Filter + +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 } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt new file mode 100644 index 0000000000..d853819a57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt new file mode 100644 index 0000000000..b14552983d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt @@ -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 { + 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 { + return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) { + is Result.Success -> Result.Success(Unit) + is Result.Failure -> result + } + } + + suspend fun checkRegistration(): Result { + 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 { + return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey) + } + + suspend fun sendTransactions( + signedTransactions: List, + ): com.tangem.blockchain.extensions.Result> { + return gnosisRegistrator.sendTransactions(signedTransactions) + } + + suspend fun registerWallet( + attestResponse: AttestWalletKeyResponse, + pinCode: String, + ): Result { + 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() + } +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt new file mode 100644 index 0000000000..11a0a30404 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt @@ -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, + 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 { + + private var generateOTPResponse: GenerateOTPResponse? = null + private var attestWalletResponse: AttestWalletKeyResponse? = null + private var signedTransactions: List = listOf() + + override fun run(session: CardSession, callback: CompletionCallback) { + generateOTP(session, callback) + } + + private fun generateOTP(session: CardSession, callback: CompletionCallback) { + 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, + ) { + 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, + ) { + 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, + session: CardSession, + callback: CompletionCallback, + ) { + 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) { + if (signedTransactions.isEmpty() || attestWalletResponse == null) { + callback(CompletionResult.Failure(TangemSdkError.UnknownError())) + } else { + val result = RegistrationTaskResponse(signedTransactions, attestWalletResponse!!) + callback(CompletionResult.Success(result)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt new file mode 100644 index 0000000000..30f0f25d71 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt @@ -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() +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt new file mode 100644 index 0000000000..1a85b11d76 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -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 = { 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 { + if (step == SaltPayRegistrationStep.KycStart || + step == SaltPayRegistrationStep.KycWaiting || + step == SaltPayRegistrationStep.Finished + ) { + return Result.Success(Unit) + } + + return saltPayManager.checkHasGas() +} + +private suspend fun registerKYCIfNeeded(state: OnboardingSaltPayState): Result { + 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 { + 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> = listOf( + AllSymbolsTheSameFilter(), + ) + + if (filters.any { it.filter(pin) }) { + throw SaltPayRegistrationError.WeakPin + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt new file mode 100644 index 0000000000..ac9ed3c67b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt new file mode 100644 index 0000000000..dfa556bbd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt @@ -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 { + 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; +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 748c7f088b..935b28b2dd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -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, 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 + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt index 8b88890be8..4d35050937 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index b51a53e29f..697dc71f67 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt b/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt deleted file mode 100644 index db7f77b99c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt +++ /dev/null @@ -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 -// } -//} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 14f9fa3397..93066fc926 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index d7e6ceba13..ba68023190 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 27d49105a4..b7748269e4 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -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 { private lateinit var warningsAdapter: WarningMessagesAdapter @@ -89,7 +88,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber(jsonData) ?: SaltPayRegistratorData() + } + isFetching = false + + return data + } + + companion object { + private const val REGISTRATION_DATA_KEY = "saltpay_registration_data" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_connecting_socket.xml b/app/src/main/res/drawable/ic_connecting_socket.xml new file mode 100644 index 0000000000..a07fbe5515 --- /dev/null +++ b/app/src/main/res/drawable/ic_connecting_socket.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_in_progress.xml b/app/src/main/res/drawable/ic_in_progress.xml new file mode 100644 index 0000000000..17f814a06d --- /dev/null +++ b/app/src/main/res/drawable/ic_in_progress.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_passport.xml b/app/src/main/res/drawable/ic_passport.xml new file mode 100644 index 0000000000..e793f05866 --- /dev/null +++ b/app/src/main/res/drawable/ic_passport.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tangem.xml b/app/src/main/res/drawable/ic_tangem.xml index 5140d0444c..93f8741777 100644 --- a/app/src/main/res/drawable/ic_tangem.xml +++ b/app/src/main/res/drawable/ic_tangem.xml @@ -1,7 +1,15 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/img_salt_pay_visa.png b/app/src/main/res/drawable/img_salt_pay_visa.png new file mode 100644 index 0000000000..19f69d5eb6 Binary files /dev/null and b/app/src/main/res/drawable/img_salt_pay_visa.png differ diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 903a3c218b..aa2c8c7494 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -28,16 +28,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipChildren="false"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="match_parent" + android:visibility="gone" /> - - - - - - + + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + app:layout_constraintStart_toStartOf="@id/btn_continue" + tools:text="+ Add backup card" /> + app:layout_constraintStart_toStartOf="parent" + tools:text="Main Action" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_buttons_common.xml b/app/src/main/res/layout/layout_onboarding_buttons_common.xml index e99f7dfe4a..9bd99de16a 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -18,13 +18,10 @@ diff --git a/app/src/main/res/layout/layout_onboarding_saltpay.xml b/app/src/main/res/layout/layout_onboarding_saltpay.xml new file mode 100644 index 0000000000..193cf0753c --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml b/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml new file mode 100644 index 0000000000..0c86022df3 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml b/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml new file mode 100644 index 0000000000..08e2bfe29b --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml new file mode 100644 index 0000000000..f1d03e8347 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml new file mode 100644 index 0000000000..efd4aed299 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_create_wallet.xml b/app/src/main/res/layout/lp_onboarding_create_wallet.xml index 3c5cd91655..68e87f3e61 100644 --- a/app/src/main/res/layout/lp_onboarding_create_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_create_wallet.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done.xml b/app/src/main/res/layout/lp_onboarding_done.xml index e6f1cb76fa..8f7be0c671 100644 --- a/app/src/main/res/layout/lp_onboarding_done.xml +++ b/app/src/main/res/layout/lp_onboarding_done.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done_activation.xml b/app/src/main/res/layout/lp_onboarding_done_activation.xml index 70be6417e0..02512421c5 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml index eeea439aad..c465795c75 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml index dc6efbcc96..e4e5587025 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml index a7ab2ca9fd..66351f4062 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml @@ -1,7 +1,7 @@ diff --git a/dependencies.gradle b/dependencies.gradle index b23c84e56f..4ac18f3aff 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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 = [