diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index ba5af7a4ce..c914631447 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -84,9 +84,9 @@ - - - + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d517123715..70b6edcebd 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 035e162458..874783a15d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ android:name="android.hardware.nfc" android:required="true" /> - @@ -132,18 +131,6 @@ - - - - - - - - () + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + internalActivity = this setContentView(R.layout.activity_main) systemActions() store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) @@ -67,18 +73,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { store.dispatch(GlobalAction.SetResources(getAndroidResources())) store.dispatch( ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this) - ) + GooglePayService(createPaymentsClient(this), this), + ), ) } - private fun getAndroidResources(): AndroidResources { return AndroidResources( AndroidResources.RString( R.string.copy_toast_msg, - R.string.details_notification_erase_wallet_not_possible - ) + R.string.details_notification_erase_wallet_not_possible, + ), ) } @@ -92,7 +97,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), - true + true, ) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT @@ -137,7 +142,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { if (snackbar != null) return snackbar = Snackbar.make( - binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE + binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE, ) if (buttonTitle != null && action != null) { snackbar?.setAction(getString(buttonTitle), action) @@ -152,12 +157,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + + onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) } when (requestCode) { LOAD_PAYMENT_DATA_REQUEST_CODE -> { store.dispatch( - ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data) + ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data), ) } } } + + override fun addOnActivityResultCallback(callback: OnActivityResultCallback) { + onActivityResultCallbacks.remove(callback) + onActivityResultCallbacks.add(callback) + } + + override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) { + onActivityResultCallbacks.remove(callback) + } +} + +lateinit var internalActivity: AppCompatActivity +//TODO: delete +fun copyToClipboard(text: String) { + internalActivity.copyToClipboard(text) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index b8acdce7b5..c0d6e566b4 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -32,8 +32,10 @@ import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig +import com.zendesk.logger.Logger import org.rekotlin.Store import timber.log.Timber +import zendesk.chat.Chat val store = Store( reducer = ::appReducer, @@ -144,12 +146,11 @@ class TapApplication : Application(), ImageLoaderFactory { infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, preferencesStorage = preferencesStorage, - logEnabled = LogConfig.zendesk, - ) - feedbackManager.initChat( - context = context, - zendeskConfig = config.zendesk!!, ) + feedbackManager.chatInitializer = { zendeskConfig -> + Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId) + Logger.setLoggable(LogConfig.zendesk) + } store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } diff --git a/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt b/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt new file mode 100644 index 0000000000..931922493c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common + +import android.content.Intent + +/** +[REDACTED_AUTHOR] + */ +interface ActivityResultCallbackHolder { + fun addOnActivityResultCallback(callback: OnActivityResultCallback) + fun removeOnActivityResultCallback(callback: OnActivityResultCallback) +} + +typealias OnActivityResultCallback = (Int, Int, Intent?) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index ef357f782f..5ae8d9ce04 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -17,6 +17,10 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.TryToInterruptRegistrationDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog @@ -51,7 +55,6 @@ class DialogManager : StoreSubscriber { store.unsubscribe(this) } - override fun newState(state: GlobalState) { if (state.dialog == null) { dialog?.dismiss() @@ -64,6 +67,8 @@ class DialogManager : StoreSubscriber { dialog = when (state.dialog) { is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context) is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) + is AppDialog.SimpleOkErrorDialog -> SimpleOkDialog.create(state.dialog, context) + is AppDialog.SimpleOkWarningDialog -> SimpleOkDialog.create(state.dialog, context) is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) @@ -73,16 +78,16 @@ class DialogManager : StoreSubscriber { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet, - context = context + context = context, ) is WalletConnectDialog.AddNetwork -> SimpleAlertDialog.create( titleRes = R.string.wallet_connect, message = context.getString( R.string.wallet_connect_network_not_found_format, - state.dialog.network + state.dialog.network, ), - context = context + context = context, ) is WalletConnectDialog.OpeningSessionRejected -> { SimpleAlertDialog.create( @@ -120,24 +125,23 @@ class DialogManager : StoreSubscriber { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_scanner_error_unsupported_network, - context = context + context = context, ) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context) is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context) - is WalletDialog.CurrencySelectionDialog -> - CurrencySelectionDialog.create(state.dialog, context) - is WalletDialog.ChooseTradeActionDialog -> - ChooseTradeActionBottomSheetDialog(context) - is WalletDialog.SelectAmountToSendDialog -> - AmountToSendBottomSheetDialog(context, state.dialog) - is WalletDialog.SignedHashesMultiWalletDialog -> - SignedHashesWarningDialog.create(context) + is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context) + is SaltPayDialog.Activation.TryToInterrupt -> TryToInterruptRegistrationDialog.create(context, state.dialog) + is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog) + is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context) + is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context) + is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog) + is WalletDialog.SignedHashesMultiWalletDialog -> SignedHashesWarningDialog.create(context) is WalletDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create( title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), message = context.getString( - state.dialog.messageRes, state.dialog.currencySymbol, state.dialog.currencyTitle + state.dialog.messageRes, state.dialog.currencySymbol, state.dialog.currencyTitle, ), context = context, ) @@ -146,7 +150,7 @@ class DialogManager : StoreSubscriber { messageRes = state.dialog.messageRes, context = context, primaryButtonRes = state.dialog.primaryButtonRes, - primaryButtonAction = state.dialog.onOk + primaryButtonAction = state.dialog.onOk, ) is WalletDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(context) diff --git a/app/src/main/java/com/tangem/tap/common/Handler.kt b/app/src/main/java/com/tangem/tap/common/Handler.kt index f36d4d1541..37b39734cb 100644 --- a/app/src/main/java/com/tangem/tap/common/Handler.kt +++ b/app/src/main/java/com/tangem/tap/common/Handler.kt @@ -15,6 +15,10 @@ fun postBackground(ms: Long = 0, func: Runnable) { if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms) } +fun postUiDelayBg(ms: Long, func: Runnable) { + backgroundHandler.postDelayed({ uiHandler.post(func) }, ms) +} + fun post(ms: Long = 0, func: Runnable) { if (ms == 0L) { func.run() diff --git a/app/src/main/java/com/tangem/tap/common/TestActions.kt b/app/src/main/java/com/tangem/tap/common/TestActions.kt index d5939dae64..5eb1021886 100644 --- a/app/src/main/java/com/tangem/tap/common/TestActions.kt +++ b/app/src/main/java/com/tangem/tap/common/TestActions.kt @@ -27,8 +27,8 @@ object TestActions { * @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig */ fun initFor(view: View, actions: List, isTestView: Boolean = false) { - if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED) if (!BuildConfig.TEST_ACTION_ENABLED) return + if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED) view.setOnClickListener { store.dispatchDialogShow(AppDialog.TestActionsDialog(actions)) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt index 58b026e655..17e31d88c4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt @@ -65,7 +65,7 @@ object TangemSdk { is TangemSdkError.Busy -> TangemSdkError.Busy() is TangemSdkError.MissingPreflightRead -> TangemSdkError.MissingPreflightRead() is TangemSdkError.WrongCardNumber -> TangemSdkError.WrongCardNumber() - is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType() + is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType(null) is TangemSdkError.CardError -> TangemSdkError.CardError() is TangemSdkError.NotSupportedFirmwareVersion -> TangemSdkError.NotSupportedFirmwareVersion() is TangemSdkError.WalletError -> TangemSdkError.WalletError() 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 new file mode 100644 index 0000000000..ad7bbe794b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt @@ -0,0 +1,172 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.text.isDigitsOnly + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PinCodeWidget( + config: PinViewConfig = tangemPinConfig, + onPinChanged: (String, Boolean) -> Unit = { pin, isLastSymbolEntered -> }, +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val rTextFieldValue = remember { mutableStateOf(TextFieldValue("")) } + val indexedSymbols: List = createPinSymbolsList(config.pinsCount, rTextFieldValue.value.text) + + fun isLastSymbolEntered(): Boolean = rTextFieldValue.value.text.length == config.pinsCount + + fun handleOnTextFieldValueChanged(value: TextFieldValue) { + if (!value.text.isDigitsOnly()) return + + if (value.text.length <= config.pinsCount) { + rTextFieldValue.value = value + onPinChanged(value.text, isLastSymbolEntered()) + } + } + + Box( + modifier = config.modifier + .pointerInput(Unit) { + detectTapGestures { + focusRequester.requestFocus() + keyboardController?.show() + } + }, + ) { + Row { + for (index in 0 until config.pinsCount) { + PinElement( + config = config, + pinSymbol = indexedSymbols[index] ?: "", + ) + } + } + TextField( + modifier = Modifier + .alpha(0f) + .size(1.dp) + .align(Alignment.Center) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = if (isLastSymbolEntered()) ImeAction.Done else ImeAction.Next, + ), + keyboardActions = KeyboardActions( + onDone = { keyboardController?.hide() }, + ), + value = rTextFieldValue.value, + onValueChange = ::handleOnTextFieldValueChanged, + ) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun PinElement( + config: PinViewConfig, + pinSymbol: String, +) { + Box(Modifier.padding(config.pinBoxPadding)) { + Box(config.pinBoxModifier) { + Text( + text = pinSymbol, + modifier = config.pinTextModifier.align(Alignment.Center), + style = config.pinsTextStyle ?: LocalTextStyle.current, + ) + } + } +} + +@Composable +private fun HiddenInputField() { +} + +private fun createPinSymbolsList(size: Int, text: String): List = List(size) { + try { + text[it].toString() + } catch (ex: IndexOutOfBoundsException) { + null + } +} + +data class PinViewConfig( + val modifier: Modifier = Modifier, + val pinBoxModifier: Modifier = Modifier, + val pinBoxPadding: Dp = 0.dp, + val pinTextModifier: Modifier = Modifier, + val pinsCount: Int = 4, + val pinsTextStyle: TextStyle? = null, +) + +private val tangemPinConfig = PinViewConfig( + modifier = Modifier + .wrapContentSize(), + pinBoxModifier = Modifier + .width(42.dp) + .height(56.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFF0F0F0)), + pinBoxPadding = 6.dp, + pinTextModifier = Modifier, + pinsCount = 4, + pinsTextStyle = TextStyle( + fontWeight = FontWeight(500), + fontSize = 24.sp, + ), +) + +@Preview +@Composable +fun PinCodeWidgetPreview() { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + PinCodeWidget(tangemPinConfig) + } +} \ No newline at end of file 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/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 71165c14df..e53e1effbe 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -28,7 +28,10 @@ fun Blockchain.getRoundIconRes(): Int { Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_round Blockchain.Gnosis -> R.drawable.ic_gnosis_round + Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_round + Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_round Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_round + Blockchain.Kusama -> R.drawable.ic_kusama_round Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism Blockchain.Dash -> R.drawable.ic_dash else -> R.drawable.ic_tangem_logo @@ -58,7 +61,10 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_no_color Blockchain.Gnosis -> R.drawable.ic_gnosis_no_color + Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_no_color + Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_no_color Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_no_color + Blockchain.Kusama -> R.drawable.ic_kusama_no_color Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color Blockchain.Dash -> R.drawable.ic_dash_no_color else -> R.drawable.ic_tangem_logo 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/ImageView.kt b/app/src/main/java/com/tangem/tap/common/extensions/ImageView.kt new file mode 100644 index 0000000000..0c3a471d54 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/ImageView.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.extensions + +import android.widget.ImageView +import androidx.annotation.DrawableRes + +/** +[REDACTED_AUTHOR] + */ +fun ImageView.setDrawable(@DrawableRes resId: Int) { + setImageDrawable(context.getDrawableCompat(resId)) +} \ 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/extensions/WebView.kt b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt new file mode 100644 index 0000000000..ebe938a1ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.extensions + +import android.webkit.WebView + +/** +[REDACTED_AUTHOR] + */ +fun WebView.configureSettings() { + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + } +} + +fun WebView.stop() { + stopLoading() + pauseTimers() + destroy() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 70912f24a6..97c8a9e41f 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -7,16 +7,19 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.address.Address +import com.tangem.common.card.CardWallet import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.stripZeroPlainString class AdditionalFeedbackInfo { class EmailWalletInfo( var blockchain: Blockchain = Blockchain.Unknown, - var address: String = "", - var explorerLink: String = "", - var host: String = "", var derivationPath: String = "", + var outputsCount: String? = null, + var host: String = "", + var addresses: String = "", + var explorerLink: String = "", ) var appVersion: String = "" @@ -48,23 +51,14 @@ class AdditionalFeedbackInfo { cardBlockchain = data.walletData?.blockchain ?: "" cardFirmwareVersion = data.card.firmwareVersion.stringValue cardIssuer = data.card.issuer.name - signedHashesCount = data.card.wallets - .joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" } + signedHashesCount = formatSignedHashes(data.card.wallets) } fun setWalletsInfo(walletManagers: List) { walletsInfo.clear() tokens.clear() walletManagers.forEach { manager -> - walletsInfo.add( - EmailWalletInfo( - blockchain = manager.wallet.blockchain, - address = getAddress(manager.wallet), - explorerLink = getExploreUri(manager.wallet), - host = manager.currentHost, - derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: "" - ) - ) + walletsInfo.add(createEmailWalletInfo(manager)) if (manager.cardTokens.isNotEmpty()) { tokens[manager.wallet.blockchain] = manager.cardTokens } @@ -72,45 +66,55 @@ class AdditionalFeedbackInfo { } fun updateOnSendError( - wallet: Wallet, - host: String, + walletManager: WalletManager, amountToSend: Amount, feeAmount: Amount, destinationAddress: String, ) { - onSendErrorWalletInfo = EmailWalletInfo( - blockchain = wallet.blockchain, - address = getAddress(wallet), - explorerLink = getExploreUri(wallet), - host = host, - derivationPath = wallet.publicKey.derivationPath?.rawPath ?: "" - ) - + onSendErrorWalletInfo = createEmailWalletInfo(walletManager) this.destinationAddress = destinationAddress amount = amountToSend.value?.stripZeroPlainString() ?: "0" fee = feeAmount.value?.stripZeroPlainString() ?: "0" token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else "" } - private fun getAddress(wallet: Wallet): String { - return if (wallet.addresses.size == 1) { - wallet.address - } else { - val addresses = wallet.addresses.joinToString(", ") { - "${it.type.javaClass.simpleName} - ${it.value}" - } - "Multiple address: $addresses" + private fun createEmailWalletInfo(walletManager: WalletManager): EmailWalletInfo { + return EmailWalletInfo( + blockchain = walletManager.wallet.blockchain, + derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", + outputsCount = walletManager.outputsCount?.toString(), + host = walletManager.currentHost, + addresses = formatAddresses(walletManager.wallet), + explorerLink = formatExploreUrls(walletManager.wallet), + ) + } + + private fun formatSignedHashes(wallets: List): String { + return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" } + } + + private fun formatAddresses(wallet: Wallet): String { + return wallet.formatAddressWith("Multiple address:") { + "${it.name} - ${it.value}" } } - private fun getExploreUri(wallet: Wallet): String { - return if (wallet.addresses.size == 1) { - wallet.getExploreUrl(wallet.address) - } else { - val links = wallet.addresses.joinToString(", ") { - "${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}" - } - "Multiple explorers links: $links" + private fun formatExploreUrls(wallet: Wallet): String { + return wallet.formatAddressWith("Multiple explorers links:") { + "${it.name} - ${wallet.getExploreUrl(it.value)}" } } + + private fun Wallet.formatAddressWith(with: String, mapAddress: (Address) -> String): String { + return if (addresses.size == 1) { + getExploreUrl(address) + } else { + addresses.map { mapAddress(it) }.toMutableList() + .apply { add(0, with) } + .joinToString("\n") + } + } + + private val Address.name: String + get() = type.javaClass.simpleName } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt index a3a9ee5575..46cf302fee 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -3,7 +3,7 @@ package com.tangem.tap.common.feedback import com.tangem.tap.common.extensions.breakLine class FeedbackDataBuilder( - private val infoHolder: AdditionalFeedbackInfo + private val infoHolder: AdditionalFeedbackInfo, ) { val builder = StringBuilder() @@ -21,7 +21,7 @@ class FeedbackDataBuilder( builder.appendKeyValue("Card ID", infoHolder.cardId) builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain) - builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount) + builder.appendKeyValue("", infoHolder.signedHashesCount) return this } @@ -29,20 +29,22 @@ class FeedbackDataBuilder( infoHolder.walletsInfo.forEach { builder.appendDelimiter() builder.appendKeyValue("Blockchain", it.blockchain.fullName) - builder.appendKeyValue("Host", it.host) - builder.appendKeyValue("Wallet address", it.address) builder.appendKeyValue("Derivation path", it.derivationPath) - builder.appendKeyValue("Explorer link", it.explorerLink) + builder.appendKeyValue("Outputs count", it.outputsCount) infoHolder.tokens[it.blockchain]?.let { tokens -> builder.append("Tokens:") breakLine() tokens.forEach { token -> - builder.appendKeyValue("Name", token.name) builder.appendKeyValue("ID", token.id ?: "[custom token]") + builder.appendKeyValue("Name", token.name) builder.appendKeyValue("Contract address", token.contractAddress) } } + + builder.appendKeyValue("Host", it.host) + builder.appendKeyValue("Wallet address", it.addresses) + builder.appendKeyValue("Explorer link", it.explorerLink) } return this } @@ -55,7 +57,7 @@ class FeedbackDataBuilder( builder.appendKeyValue("Token", infoHolder.token) builder.appendKeyValue("Error", error) builder.appendDelimiter() - builder.appendKeyValue("Source address", walletInfo.address) + builder.appendKeyValue("Source address", walletInfo.addresses) builder.appendKeyValue("Destination address", infoHolder.destinationAddress) builder.appendKeyValue("Amount", infoHolder.amount) builder.appendKeyValue("Fee", infoHolder.fee) @@ -72,8 +74,10 @@ class FeedbackDataBuilder( fun build(): String = builder.toString() } -private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder { - return if (value.isNotBlank()) this.append("$key: $value\n") else this +private fun StringBuilder.appendKeyValue(key: String, value: String?): StringBuilder = when { + value.isNullOrBlank() -> this + key.isBlank() -> this.append("$value\n") + else -> this.append("$key: $value\n") } private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 1631952ce6..12fe1e42e9 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -10,7 +10,6 @@ import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -import com.zendesk.logger.Logger import timber.log.Timber import zendesk.chat.Chat import zendesk.chat.ChatConfiguration @@ -30,20 +29,17 @@ class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val preferencesStorage: PreferencesStorage, - private val logEnabled: Boolean = false, ) { - fun initChat( - context: Context, - zendeskConfig: ZendeskConfig, - ) { - Chat.INSTANCE.init( - context, - zendeskConfig.accountKey, - zendeskConfig.appId - ) + private var lastUsedConfigForInitialization: ZendeskConfig? = null - // Zendesk logs - Logger.setLoggable(logEnabled) + var chatInitializer: ((ZendeskConfig) -> Unit)? = null + + fun initChat(zendeskConfig: ZendeskConfig) { + // prevent double initialization with the same config + if (lastUsedConfigForInitialization == zendeskConfig) return + + lastUsedConfigForInitialization = zendeskConfig + chatInitializer?.invoke(zendeskConfig) } fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { @@ -55,7 +51,7 @@ class FeedbackManager( subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), file = fileLog, - onFail = onFail + onFail = onFail, ) } } 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/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/StateDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt index 89d609b32c..e216493dd6 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt @@ -12,19 +12,22 @@ interface StateDialog sealed class AppDialog : StateDialog { data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog() + data class SimpleOkErrorDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog() + data class SimpleOkWarningDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog() data class SimpleOkDialogRes( val headerId: Int, val messageId: Int, - val onOk: VoidCallback? = null + val onOk: VoidCallback? = null, ) : AppDialog() object ScanFailsDialog : AppDialog() + data class AddressInfoDialog( val currency: Currency, val addressData: AddressData, ) : AppDialog() data class TestActionsDialog( - val actionsList: List + val actionsList: List, ) : AppDialog() } \ No newline at end of file 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 8c50713103..b226be3401 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 @@ -14,6 +14,7 @@ import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.ToastNotificationAction +import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -36,9 +37,22 @@ 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() { + /** + * Initiate an onboarding process. + * For SaltPay cards it's additionally checks for unfinished backup. + * For resuming unfinished backup for standard Wallet cards see CheckForUnfinishedBackup and + * StartForUnfinishedBackup + */ + data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding() + + /** + * Initiate resuming of unfinished backup only for standard Wallet cards. + * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. + * See more Onboarding.Start, CheckForUnfinishedBackup + */ + object StartForUnfinishedBackup : Onboarding() + object Stop : Onboarding() } data class ScanCard( @@ -78,7 +92,7 @@ sealed class GlobalAction : Action { data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() - data class OpenChat(val feedbackData: FeedbackData) : GlobalAction() + data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 0b938c6bf7..a805bbd798 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -87,7 +87,30 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) } is GlobalAction.OpenChat -> { - store.state.globalState.feedbackManager?.openChat(action.feedbackData) + val globalState = store.state.globalState + val feedbackManager = globalState.feedbackManager.guard { + store.dispatchDebugErrorNotification("FeedbackManager not initialized") + return + } + val config = globalState.configManager?.config.guard { + store.dispatchDebugErrorNotification("Config not initialized") + return + } + + val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse + + // if config not set -> try to get it based on a scanResponse.productType + val unsafeZendeskConfig = action.zendeskConfig ?: when { + scanResponse?.isSaltPay() == true -> config.saltPayConfig?.zendesk + else -> config.zendesk + } + + val zendeskConfig = unsafeZendeskConfig.guard { + store.dispatchDebugErrorNotification("ZendeskConfig not initialized") + return + } + feedbackManager.initChat(zendeskConfig) + feedbackManager.openChat(action.feedbackData) } is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) 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 e8ae540deb..b0cefc6d9e 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/common/toggleWidget/RefreshBalanceWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt index 277f839c05..7bccdffc08 100644 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt @@ -2,7 +2,14 @@ package com.tangem.tap.common.toggleWidget import android.view.View import android.view.ViewGroup -import android.view.animation.* +import android.view.animation.AccelerateInterpolator +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.AnimationSet +import android.view.animation.AnticipateOvershootInterpolator +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.view.animation.ScaleAnimation import android.widget.ViewSwitcher import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.wallet.R @@ -11,7 +18,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ class RefreshBalanceWidget( - private val root: ViewGroup + private val root: ViewGroup, ) : ViewStateWidget { var isShowing: Boolean? = null @@ -19,7 +26,6 @@ class RefreshBalanceWidget( override val mainView: View = root.findViewById(R.id.btn_refresh_balance) - private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) } private val arrowView = mainView.findViewById(R.id.imv_arrow_refresh) @@ -46,6 +52,7 @@ class RefreshBalanceWidget( currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return } + mainView.isClickable = currentStateByView == ProgressState.Done || currentStateByView == ProgressState.Error animateState(progressState) viewSwitcher.showNext() } @@ -60,7 +67,7 @@ class RefreshBalanceWidget( progressViewAnimation = RotateAnimation( 0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f + Animation.RELATIVE_TO_SELF, 0.5f, ) progressViewAnimation?.duration = 700 progressViewAnimation?.interpolator = AccelerateInterpolator() @@ -88,11 +95,13 @@ class RefreshBalanceWidget( class ShowAnimation : AnimationSet(true) { init { - addAnimation(ScaleAnimation( - 0f, 1f, - 0f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f) + addAnimation( + ScaleAnimation( + 0f, 1f, + 0f, 1f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f, + ), ) addAnimation(AlphaAnimation(0f, 1f)) duration = 300 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 b4a63f3277..77d70bf042 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 @@ -59,32 +60,32 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } suspend fun createProductWallet( - scanResponse: ScanResponse + scanResponse: ScanResponse, ): CompletionResult { return runTaskAsync( CreateProductWalletTask(scanResponse.productType), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)) + Message(context.getString(R.string.initial_message_create_wallet_body)), ) } private fun sendScanResultsToAnalytics( analyticsHandler: GlobalAnalyticsEventHandler?, - result: CompletionResult + result: CompletionResult, ) { when (result) { is CompletionResult.Success -> { analyticsHandler?.handleAnalyticsEvent( event = AnalyticsEvent.CARD_IS_SCANNED, card = result.data.card, - blockchain = result.data.walletData?.blockchain + blockchain = result.data.walletData?.blockchain, ) if (DemoHelper.isDemoCard(result.data)) { analyticsHandler?.handleAnalyticsEvent( event = AnalyticsEvent.DEMO_MODE_ACTIVATED, params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId), card = result.data.card, - blockchain = result.data.walletData?.blockchain + blockchain = result.data.walletData?.blockchain, ) } } @@ -92,20 +93,23 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co (result.error as? TangemSdkError)?.let { error -> analyticsHandler?.handleCardSdkErrorEvent( error = error, - action = Analytics.ActionToLog.Scan + action = Analytics.ActionToLog.Scan, ) } } } suspend fun createWallet(cardId: String?): CompletionResult { - return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body))) + return runTaskAsyncReturnOnMain( + CreateWalletAndRescanTask(), + cardId, + initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), + ) } suspend fun derivePublicKeys( cardId: String, - derivations: Map> + derivations: Map>, ): CompletionResult { return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } @@ -114,49 +118,58 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( ResetToFactorySettingsTask(), card.cardId, - initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings))) + initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)), + ) } suspend fun setPasscode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body))) + initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)), + ) } suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body))) + initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)), + ) } suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun checkUserCodes(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( CheckUserCodesCommand(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun scanCard(): CompletionResult { return runTaskAsyncReturnOnMain( ScanTask(), - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun runTaskAsync( - runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, + runnable: CardSessionRunnable, + cardId: String? = null, + initialMessage: Message? = null, + accessCode: String? = null, ): CompletionResult = withContext(Dispatchers.Main) { suspendCoroutine { continuation -> - tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result -> + tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result -> if (continuation.context.isActive) continuation.resume(result) } } @@ -170,20 +183,24 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) { - tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) { - CardIdDisplayFormat.LastLuhn(4) - } else { - CardIdDisplayFormat.Full + tangemSdk.config.cardIdDisplayFormat = when { + scanResponse.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4) + scanResponse.isSaltPay() -> CardIdDisplayFormat.None + else -> CardIdDisplayFormat.Full } } + fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + return context.getString(stringResId, formatArgs) + } + companion object { val config = Config( linkedTerminal = true, allowUntrustedCards = true, filter = CardFilter( - allowedCardTypes = FirmwareVersion.FirmwareType.values().toList() - ) + allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt index 9e22833682..af5cdb6a5c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt @@ -14,6 +14,7 @@ class TangemSigner( private val card: Card, private val tangemSdk: TangemSdk, private val initialMessage: Message, + private val accessCode: String? = null, private val signerCallback: (TangemSignerResponse) -> Unit, ) : TransactionSigner { @@ -29,6 +30,7 @@ class TangemSigner( runnable = task, cardId = cardId, initialMessage = initialMessage, + accessCode = accessCode, ) { result -> when (result) { is CompletionResult.Success -> { diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index a2b994507a..5a0cf85668 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -42,7 +42,7 @@ sealed class TapError( data class UnsupportedState( val stateError: String, - val customMessage: String = "Unsupported state:" + val customMessage: String = "Unsupported state:", ) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError")) sealed class WalletManager { @@ -53,9 +53,14 @@ sealed class TapError( object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later) } + sealed class WalletConnect { + object UnsupportedDapp : TapError(R.string.wallet_connect_error_unsupported_dapp) + object UnsupportedLink : TapError(R.string.wallet_connect_error_failed_to_connect) + } + data class ValidateTransactionErrors( override val errorList: List, - override val builder: (List) -> String + override val builder: (List) -> String, ) : TapError(-1), MultiMessageError } diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 4f64bc1d20..b15f2a7431 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -35,9 +35,11 @@ class TapWalletManager { val walletManagerFactory: WalletManagerFactory by lazy { WalletManagerFactory(blockchainSdkConfig) } val rates: RatesRepository = RatesRepository() + private val blockchainSdkConfig by lazy { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } + private val walletManagersThrottler = ThrottlerWithValues>(10000) @@ -132,42 +134,10 @@ class TapWalletManager { } dispatchOnMain(WalletAction.LoadWallet()) - dispatchOnMain(WalletAction.LoadFiatRate()) } - private suspend fun loadMultiWalletData( - scanResponse: ScanResponse, - ) { - when (val tokensResult = userTokensRepository.getUserTokens(scanResponse.card)) { - is Result.Success -> { - withMainContext { - val blockchainNetworks = tokensResult.data.toBlockchainNetworks() - val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, tokensResult.data) - store.dispatch( - WalletAction.MultiWallet.AddBlockchains( - blockchains = blockchainNetworks, - walletManagers = walletManagers, - save = false, - ), - ) - - blockchainNetworks.filter { it.tokens.isNotEmpty() } - .map { - store.dispatch( - WalletAction.MultiWallet.AddTokens( - tokens = it.tokens, - blockchain = it, - save = false, - ), - ) - } - checkIfDerivationsAreMissing(blockchainNetworks, scanResponse) - } - } - is Result.Failure -> { - return - } - } + private suspend fun loadMultiWalletData(scanResponse: ScanResponse) { + loadUserCurrencies(scanResponse, walletManagerFactory) } private fun checkIfDerivationsAreMissing(blockchainNetworks: List, scanResponse: ScanResponse) { @@ -203,11 +173,41 @@ class TapWalletManager { walletManagers = listOf(primaryWalletManager), save = false, ), + WalletAction.LoadFiatRate(), ) } } + private suspend fun loadUserCurrencies(scanResponse: ScanResponse, walletManagerFactory: WalletManagerFactory) { + val userTokens = userTokensRepository.getUserTokens(scanResponse.card) + withMainContext { + val blockchainNetworks = userTokens.toBlockchainNetworks() + val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, userTokens) + store.dispatch( + WalletAction.MultiWallet.AddBlockchains( + blockchains = blockchainNetworks, + walletManagers = walletManagers, + save = false, + ), + ) + + blockchainNetworks.filter { it.tokens.isNotEmpty() } + .map { + store.dispatch( + WalletAction.MultiWallet.AddTokens( + tokens = it.tokens, + blockchain = it, + save = false, + ), + ) + } + checkIfDerivationsAreMissing(blockchainNetworks, scanResponse) + store.dispatch(WalletAction.LoadFiatRate(coinsList = userTokens)) + } + } + suspend fun reloadData(data: ScanResponse) { + loadUserCurrencies(data, walletManagerFactory) withContext(Dispatchers.Main) { getActionIfUnknownBlockchainOrEmptyWallet(data)?.let { store.dispatch(it) @@ -219,7 +219,6 @@ class TapWalletManager { } store.dispatch(WalletAction.LoadWallet()) - store.dispatch(WalletAction.LoadFiatRate()) } } 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 81ce11fd37..bd530b0796 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] @@ -23,6 +24,7 @@ data class Config( val isCreatingTwinCardsAllowed: Boolean = false, val shopify: ShopifyShop? = null, val zendesk: ZendeskConfig? = null, + val saltPayConfig: SaltPayConfig? = null, ) class ConfigManager { @@ -92,6 +94,7 @@ class ConfigManager { amplitudeApiKey = values.amplitudeApiKey, shopify = values.shopifyShop, zendesk = values.zendesk, + saltPayConfig = values.saltPay, ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, @@ -109,6 +112,7 @@ class ConfigManager { amplitudeApiKey = values.amplitudeApiKey, 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 16af7f21e1..1c537e7909 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,6 +2,7 @@ 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] @@ -27,6 +28,7 @@ class ConfigValueModel( val appsFlyerDevKey: String, val shopifyShop: ShopifyShop?, val zendesk: ZendeskConfig?, + val saltPay: SaltPayConfig, val tronGridApiKey: String, val amplitudeApiKey: String, ) 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 c627a7de91..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 @@ -6,6 +6,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result +import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemNote import com.tangem.domain.common.TwinCardNumber @@ -23,11 +24,14 @@ val Card.isWalletDataSupported: Boolean val Card.isMultiwalletAllowed: Boolean get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote() + return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || getSingleWallet()?.curve == EllipticCurve.Secp256k1) } +val Card.isHdWalletAllowedByApp: Boolean + get() = settings.isHDWalletAllowed && !isSaltPay + fun Card.getSingleWallet(): CardWallet? { return wallets.firstOrNull() } 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 90981e2359..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 @@ -39,10 +43,10 @@ fun WalletManagerFactory.makeWalletManagerForApp( walletPublicKey = wallet.publicKey, pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(), blockchain = environmentBlockchain, - curve = wallet.curve + curve = wallet.curve, ) } - seedKey != null && derivationParams != null -> { + scanResponse.card.isHdWalletAllowedByApp && (seedKey != null && derivationParams != null) -> { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] val derivationPath = when (derivationParams) { is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) @@ -55,33 +59,33 @@ fun WalletManagerFactory.makeWalletManagerForApp( blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, - derivation = derivationParams + derivation = derivationParams, ) } else -> { 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/moduleMessage/ConvertedMessage.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ConvertedMessage.kt new file mode 100644 index 0000000000..a8add4f2a5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ConvertedMessage.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.domain.moduleMessage + +/** +[REDACTED_AUTHOR] + * Base interface for conversion outputs from ModuleMessageConverter + */ +interface ConvertedMessage { + val message: String +} + +/** + * Dialog message used to construct a android.Dialog + */ +interface DialogMessage : ConvertedMessage { + val title: String + override val message: String + val onPositive: String? + val onNegative: String? + val onNeutral: String? +} + +data class ConvertedStringMessage( + override val message: String, +) : ConvertedMessage + +data class ConvertedDialogMessage( + override val title: String, + override val message: String, + override val onPositive: String? = null, + override val onNegative: String? = null, + override val onNeutral: String? = null, +) : DialogMessage \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt new file mode 100644 index 0000000000..7c665e5cdd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.domain.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.domain.moduleMessage.domain.DomainMessageConverter +import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayMessageConverter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage + +class ModuleMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: ModuleMessage): ConvertedMessage { + val convertedMessage = when (message) { + is DomainModuleMessage -> DomainMessageConverter(context).convert(message) + is SaltPayModuleMessage -> SaltPayMessageConverter(context).convert(message) + else -> null + } + return convertedMessage ?: convertUnknownMessage(message) + } + + private fun convertUnknownMessage(message: ModuleMessage): ConvertedMessage { + return ConvertedStringMessage("Unknown message: ${message::class.java.simpleName}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt new file mode 100644 index 0000000000..67c58be34a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.moduleMessage.domain + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.domain.AddCustomTokenError +import com.tangem.domain.DomainModuleError +import com.tangem.domain.DomainModuleMessage +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.domain.converter.AddCustomTokenErrorConverter + +/** +[REDACTED_AUTHOR] + */ +class DomainMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: DomainModuleMessage): ConvertedMessage? { + return when (message) { + is DomainModuleError -> DomainErrorConverter(context).convert(message) + else -> null + } + } +} + +class DomainErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: DomainModuleError): ConvertedMessage? = when (message) { + is AddCustomTokenError -> AddCustomTokenErrorConverter(context).convert(message) + else -> null + } +} \ 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/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt similarity index 68% rename from app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt rename to app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt index 7f5221041e..66e41dc48e 100644 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt @@ -1,28 +1,21 @@ -package com.tangem.tap.common.moduleMessage.domain +package com.tangem.tap.domain.moduleMessage.domain.converter import android.content.Context import com.tangem.common.module.ModuleMessageConverter import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainError +import com.tangem.domain.DomainModuleError +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.ConvertedStringMessage 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 - } -} +internal class AddCustomTokenErrorConverter( + private val context: Context, +) : ModuleMessageConverter { -private class AddCustomTokenConverter( - private val context: Context -) : ModuleMessageConverter { - - override fun convert(message: DomainError): String? { + override fun convert(message: DomainModuleError): ConvertedMessage? { val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException() val rawMessage = when (customTokenError) { @@ -40,8 +33,8 @@ private class AddCustomTokenConverter( } return when (rawMessage) { - is Int -> context.getString(rawMessage) - is String -> rawMessage + is Int -> ConvertedStringMessage(context.getString(rawMessage)) + is String -> ConvertedStringMessage(rawMessage) else -> null } } diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt new file mode 100644 index 0000000000..ac8c8cd359 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.moduleMessage.saltPay + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.saltPay.converter.SaltPayActivationErrorConverter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage + +/** +[REDACTED_AUTHOR] + */ +class SaltPayMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayModuleMessage): ConvertedMessage? { + return when (message) { + is SaltPayModuleError -> SaltPayErrorConverter(context).convert(message) + else -> null + } + } +} + +class SaltPayErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) { + is SaltPayActivationError -> SaltPayActivationErrorConverter(context).convert(message) + else -> null + } +} diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayActivationErrorConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayActivationErrorConverter.kt new file mode 100644 index 0000000000..65de384d44 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayActivationErrorConverter.kt @@ -0,0 +1,53 @@ +package com.tangem.tap.domain.moduleMessage.saltPay.converter + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +internal class SaltPayActivationErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayActivationError): ConvertedDialogMessage? { + val saltPayError = (message as? SaltPayActivationError) ?: throw UnsupportedOperationException() + + val dialogMessage = when (saltPayError) { + SaltPayActivationError.NoGas -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_no_gas_title), + message = context.getString(R.string.saltpay_error_no_gas_message), + ) + } + SaltPayActivationError.EmptyBackupCardScanned -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_empty_backup_title), + message = context.getString(R.string.saltpay_error_empty_backup_message), + ) + } + SaltPayActivationError.WeakPin -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_pin_weak_title), + message = context.getString(R.string.saltpay_error_pin_weak_message), + ) + } + else -> { + val errorMessage = if (saltPayError.message == SaltPayActivationError.EMPTY_MESSAGE) { + saltPayError::class.java.simpleName + } else { + saltPayError.message + } + ConvertedDialogMessage( + title = context.getString(R.string.common_error), + message = errorMessage, + ) + } + } + + return dialogMessage + } +} \ 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 485ee0eaea..252d3585e6 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 @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError @@ -18,10 +17,11 @@ import com.tangem.domain.common.ProductType 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.isTangemNote +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 @@ -32,9 +32,12 @@ 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.extensions.isMultiwalletAllowed import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import kotlinx.coroutines.launch class ScanProductTask( val card: Card? = null, @@ -58,8 +61,8 @@ class ScanProductTask( } val commandProcessor = when { - card.isTangemNote() -> ScanNoteProcessor() - card.isTangemTwins() -> ScanTwinProcessor() + card.isTangemNote -> ScanNoteProcessor() + card.isTangemTwins -> ScanTwinProcessor() else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive) } commandProcessor.proceed(card, session) { processorResult -> @@ -83,9 +86,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 return null } } @@ -121,12 +123,13 @@ private class ScanWalletProcessor( ) { createMissingWalletsIfNeeded(card, session, callback) } + private fun createMissingWalletsIfNeeded( card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + if (card.wallets.isEmpty() || !card.isMultiwalletAllowed) { startLinkingForBackupIfNeeded(card, session, callback) return } @@ -182,39 +185,45 @@ private class ScanWalletProcessor( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val derivations = collectDerivations(card) - if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { - callback( - CompletionResult.Success( - ScanResponse( - card = card, - productType = ProductType.Wallet, - walletData = session.environment.walletData, - primaryCard = primaryCard, - ), - ), - ) - return + val productType = when (card.isSaltPay) { + true -> ProductType.SaltPay + else -> ProductType.Wallet } + scope.launch { + val derivations = collectDerivations(card) + if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { + callback( + CompletionResult.Success( + ScanResponse( + card = card, + productType = productType, + walletData = session.environment.walletData, + primaryCard = primaryCard, + ), + ), + ) + return@launch + } - DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result -> - when (result) { - is CompletionResult.Success -> { - val response = ScanResponse( - card = card, - productType = ProductType.Wallet, - walletData = session.environment.walletData, - derivedKeys = result.data.entries, - primaryCard = primaryCard, - ) - callback(CompletionResult.Success(response)) + DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + val response = ScanResponse( + card = card, + productType = productType, + walletData = session.environment.walletData, + derivedKeys = result.data.entries, + primaryCard = primaryCard, + ) + callback(CompletionResult.Success(response)) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } } } - private fun getBlockchainsToDerive(card: Card): List { + private suspend fun getBlockchainsToDerive(card: Card): List { val userTokensRepository = userTokensRepository ?: return emptyList() val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty { mutableListOf( @@ -248,7 +257,7 @@ private class ScanWalletProcessor( return blockchainsToDerive.distinct() } - private fun collectDerivations(card: Card): Map> { + private suspend fun collectDerivations(card: Card): Map> { val blockchains = getBlockchainsToDerive(card) val derivations = mutableMapOf>() @@ -256,6 +265,7 @@ private class ScanWalletProcessor( val curve = blockchain.blockchain.getPrimaryCurve() val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach if (wallet.chainCode == null) return@forEach + val key = wallet.publicKey.toMapKey() val path = blockchain.derivationPath?.let { DerivationPath(it) } if (path != null) { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt index ad188c6913..c0b7e18155 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt @@ -5,7 +5,6 @@ import com.tangem.common.services.Result import com.tangem.network.api.tangemTech.TangemTechService import com.tangem.network.api.tangemTech.UserTokensResponse import com.tangem.tap.domain.NoDataError -import com.tangem.tap.features.wallet.models.Currency class UserTokensNetworkService(private val tangemTechService: TangemTechService) { suspend fun getUserTokens(userId: String): Result { @@ -22,9 +21,7 @@ class UserTokensNetworkService(private val tangemTechService: TangemTechService) } } - suspend fun saveUserTokens(userId: String, tokens: List): Result { - val tokensResponse = tokens.map { it.toTokenResponse() } - val data = UserTokensResponse(tokens = tokensResponse) - return tangemTechService.putUserTokens(userId, data) + suspend fun saveUserTokens(userId: String, tokens: UserTokensResponse): Result { + return tangemTechService.putUserTokens(userId, tokens) } } 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 58c3fb08a4..34330160a0 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 @@ -8,6 +8,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.extensions.calculateHmacSha256 import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.network.api.tangemTech.UserTokensResponse import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.domain.NoDataError import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -24,20 +25,21 @@ class UserTokensRepository( private val storageService: UserTokensStorageService, private val networkService: UserTokensNetworkService, ) { - suspend fun getUserTokens(card: Card): Result> { - if (DemoHelper.isDemoCardId(card.cardId)) { - return Result.Success(loadDemoCurrencies()) - } + suspend fun getUserTokens(card: Card): List { val userId = card.getUserId() + if (DemoHelper.isDemoCardId(card.cardId)) { + return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() } + } + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { - return Result.Success(loadTokensOffline(card, userId)) + return loadTokensOffline(card, userId) } return when (val networkResult = networkService.getUserTokens(userId)) { is Result.Success -> { - val tokens = networkResult.data.tokens.map { Currency.fromTokenResponse(it) } - storageService.saveUserTokens(card.getUserId(), tokens) - Result.Success(tokens) + val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) } + storageService.saveUserTokens(card.getUserId(), tokens.toUserTokensResponse()) + tokens.distinct() } is Result.Failure -> { handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error) @@ -46,17 +48,36 @@ class UserTokensRepository( } suspend fun saveUserTokens(card: Card, tokens: List) { - networkService.saveUserTokens(card.getUserId(), tokens) - storageService.saveUserTokens(card.getUserId(), tokens) + val userTokens = tokens.toUserTokensResponse() + networkService.saveUserTokens(card.getUserId(), userTokens) + storageService.saveUserTokens(card.getUserId(), userTokens) } suspend fun removeUserTokens(card: Card) { - networkService.saveUserTokens(card.getUserId(), emptyList()) - storageService.saveUserTokens(card.getUserId(), emptyList()) + val userTokens = emptyList().toUserTokensResponse() + networkService.saveUserTokens(card.getUserId(), userTokens) + storageService.saveUserTokens(card.getUserId(), userTokens) } - fun loadBlockchainsToDerive(card: Card): List { - return storageService.getUserTokens(card.getUserId())?.toBlockchainNetworks() ?: emptyList() + private fun List.toUserTokensResponse(): UserTokensResponse { + val tokensResponse = this.map { it.toTokenResponse() } + return UserTokensResponse( + tokens = tokensResponse, + group = GROUP_DEFAULT_VALUE, + sort = SORT_DEFAULT_VALUE, + ) + } + + suspend fun loadBlockchainsToDerive(card: Card): List { + val userId = card.getUserId() + val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks() + + if (DemoHelper.isDemoCardId(card.cardId)) { + return blockchainNetworks + .ifEmpty { loadDemoCurrencies().toBlockchainNetworks() } + } + + return blockchainNetworks } private fun loadDemoCurrencies(): List { @@ -73,16 +94,17 @@ class UserTokensRepository( card: Card, userId: String, error: Throwable, - ): Result> { + ): List { return when (error) { is NoDataError -> { val tokens = storageService.getUserTokens(card) - coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = tokens) } } - Result.Success(tokens) + val userTokens = tokens.toUserTokensResponse() + coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } } + tokens } else -> { val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) - Result.Success(tokens) + tokens.distinct() } } } @@ -93,17 +115,12 @@ 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 = "AccountID" + const val SORT_DEFAULT_VALUE = "manual" + const val GROUP_DEFAULT_VALUE = "none" fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository { val fileReader = AndroidFileReader(context) val oldUserTokensRepository = OldUserTokensRepository( @@ -114,4 +131,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/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index f2011cbebb..c71e5afa53 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -20,7 +20,7 @@ class UserTokensStorageService( fun getUserTokens(userId: String): List? { return try { val json = fileReader.readFile(getFileNameForUserTokens(userId)) - userTokensAdapter.fromJson(json)?.tokens?.map { Currency.fromTokenResponse(it) } + userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) } } catch (exception: Exception) { Log.error { exception.stackTraceToString() } null @@ -34,10 +34,8 @@ class UserTokensStorageService( return blockchainNetworks.flatMap { it.toCurrencies() } } - fun saveUserTokens(userId: String, tokens: List) { - val tokensResponse = tokens.map { it.toTokenResponse() } - val data = UserTokensResponse(tokens = tokensResponse) - val json = userTokensAdapter.toJson(data) + fun saveUserTokens(userId: String, tokens: UserTokensResponse) { + val json = userTokensAdapter.toJson(tokens) fileReader.rewriteFile(json, getFileNameForUserTokens(userId)) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index 8bea1d2aeb..a3ec71fb94 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -8,6 +8,8 @@ import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.logWcEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.walletconnect.extensions.isDappSupported import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession @@ -58,7 +60,15 @@ class WalletConnectManager { private var sessions: MutableMap = mutableMapOf() fun connect(wcUri: String) { - val session = WCSession.from(wcUri) ?: return + val session = WCSession.from(wcUri).guard { + store.dispatchOnMain( + WalletConnectAction.FailureEstablishingSession( + session = null, + error = TapError.WalletConnect.UnsupportedLink, + ), + ) + return + } if (sessions[session] != null) { store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession) return @@ -66,7 +76,19 @@ class WalletConnectManager { val client = WCClient(httpClient = okHttpClient) setListeners(client) val peerId = UUID.randomUUID().toString() - client.connect(session, tangemPeerMeta, peerId) + + try { + client.connect(session, tangemPeerMeta, peerId) + } catch (exception: IllegalArgumentException) { + store.dispatchOnMain( + WalletConnectAction.FailureEstablishingSession( + session = null, + error = TapError.WalletConnect.UnsupportedLink, + ), + ) + return + } + sessions[session] = WalletConnectActiveData( peerId = peerId, remotePeerId = null, @@ -332,21 +354,30 @@ class WalletConnectManager { val session = client.session val data = sessions[session]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) if (data != null && session != null) { - sessions[session] = data - val sessionData = data.toWalletConnectSession() - sessionData?.let { + if (!peer.isDappSupported()) { store.dispatchOnMain( - WalletConnectAction.ScanCard( - session = sessionData, - chainId = client.chainId?.toIntOrNull() + WalletConnectAction.FailureEstablishingSession( + session = session, + error = TapError.WalletConnect.UnsupportedDapp, + ), + ) + } else { + sessions[session] = data + val sessionData = data.toWalletConnectSession() + sessionData?.let { + store.dispatchOnMain( + WalletConnectAction.ScanCard( + session = sessionData, + chainId = client.chainId?.toIntOrNull(), + ), ) + } + store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.WcAnalyticsEvent.Session( + Analytics.WcSessionEvent.Connect, peer.url, + ), ) } - store.state.globalState.analyticsHandler?.logWcEvent( - Analytics.WcAnalyticsEvent.Session( - Analytics.WcSessionEvent.Connect, peer.url - ) - ) } } client.onSessionUpdate = { id: Long, update: WCSessionUpdate -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt new file mode 100644 index 0000000000..057caf7bd2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.domain.walletconnect.extensions + +import com.trustwallet.walletconnect.models.WCPeerMeta + +fun WCPeerMeta.isDappSupported(): Boolean { + return !unsupportedDappsList.any { this.url.contains(it) } +} + +private val unsupportedDappsList: List = listOf("dydx.exchange") \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index 8b4d0d3f70..f0e6016021 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -7,7 +7,10 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.tangem.common.extensions.VoidCallback @@ -42,6 +45,17 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP store.dispatch(NavigationAction.PopBackTo()) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loadToolbarMenu()?.let { + val menuHost = requireActivity() as? MenuHost ?: return + menuHost.addMenuProvider(it, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + } + + protected open fun loadToolbarMenu():MenuProvider? = null + fun showRetrySnackbar(message: String, action: VoidCallback) { val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE) snackbar.setAction(getString(R.string.common_retry)) { diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 650ce5ea09..b0d473cde0 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -448,6 +448,33 @@ class DemoConfig { "AB02000000049533", "AB02000000049541", "AB02000000049830", + // === more cids === + "AC03000000091418", + "AC03000000091400", + "AC03000000099007", + "AC03000000098991", + "AC03000000098942", + "AC03000000091715", + "AC03000000091301", + "AC03000000091343", + "AB01000000055705", + "AB01000000052918", + "AB01000000047710", + "AB01000000052306", + "AB01000000047645", + "AB01000000048957", + "AB01000000052900", + "AB01000000050391", + "AB01000000047363", + "AB02000000053998", + "AB02000000019809", + "AB02000000020872", + "AB02000000022027", + "AB02000000058955", + "AB02000000053253", + "AB02000000048063", + "AB02000000023736", + "AB02000000058187", ) private val testDemoCardIds = listOf( 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 daf25b5425..12f4fee9e2 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 b327e24452..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.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/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 0258af7316..67b215dd51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.NotificationAction +import com.tangem.tap.domain.TapError import com.tangem.wallet.R import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder @@ -52,7 +53,7 @@ sealed class WalletConnectAction : Action { val updatedSession: WalletConnectSession, ) : WalletConnectAction() - data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction() + data class FailureEstablishingSession(val session: WCSession?, val error: TapError? = null) : WalletConnectAction() data class SetSessionsRestored(val sessions: List) : WalletConnectAction() @@ -103,6 +104,6 @@ sealed class WalletConnectAction : Action { data class Sign( val id: Long, val data: ByteArray, val sessionData: WCSession, - ) : WalletConnectAction() + ) : WalletConnectAction() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index cad8f8ab14..ed2c68c8d6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen @@ -22,6 +23,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.wallet.R import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -96,6 +98,16 @@ class WalletConnectMiddleware { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout)) } is WalletConnectAction.FailureEstablishingSession -> { + if (action.error != null) { + store.dispatch( + GlobalAction.ShowDialog( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = action.error.messageResource, + ), + ), + ) + } if (action.session != null) { walletConnectManager.disconnect(action.session) } @@ -306,9 +318,11 @@ class WalletConnectMiddleware { } else { blockchain } + val derivation = blockchainToMake.derivationPath(store.state.globalState.scanResponse?.card?.derivationStyle) + ?.rawPath val blockchainNetwork = BlockchainNetwork( blockchain = blockchainToMake, - derivationPath = wallet.derivationPath?.rawPath, + derivationPath = derivation, tokens = emptyList(), ) return walletState.getWalletManager(blockchainNetwork) 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/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 24129ec514..e0b4f1f186 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -86,7 +87,7 @@ fun WalletConnectDetailsItem( ) { Row( modifier = modifier - .height(84.dp) + .defaultMinSize(minHeight = 84.dp) .fillMaxWidth() .clickable { onItemsClick(SettingsElement.WalletConnect) }, horizontalArrangement = Arrangement.Start, @@ -99,7 +100,7 @@ fun WalletConnectDetailsItem( tint = colorResource(id = R.color.all_colors_azure), ) Column( - modifier = modifier.height(56.dp), + modifier = modifier.defaultMinSize(minHeight = 56.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center, ) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 97ce019a84..010b27c362 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.details +import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.SupportInfo @@ -25,7 +26,9 @@ class DetailsViewModel(private val store: Store) { if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null } SettingsElement.LinkMoreCards -> { - if (state.createBackupAllowed) it else null + // if (state.createBackupAllowed) it else null + // TODO: SaltPay: temporary excluding backup process for Visa cards + if (state.createBackupAllowed && state.scanResponse?.card?.isSaltPay != true) it else null } SettingsElement.PrivacyPolicy -> { if (state.privacyPolicyUrl != null) it else null @@ -75,8 +78,7 @@ class DetailsViewModel(private val store: Store) { store.dispatch(DetailsAction.CreateBackup) } SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + store.dispatch(DisclaimerAction.Show()) } SettingsElement.TermsOfUse -> { store.dispatch(DetailsAction.ShowDisclaimer) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index eda5e31bcc..6f29a508b1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.details.ui.resetcard import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -89,11 +91,14 @@ fun ResetCardView( color = colorResource(id = R.color.text_secondary), ) - Spacer(modifier = modifier.size(44.dp)) + Spacer(modifier = modifier.size(28.dp)) Row( modifier = modifier .fillMaxWidth() - .padding(end = 20.dp), + .clickable( + onClick = { state.onAcceptWarningToggleClick(!state.accepted) }, + ) + .padding(top = 16.dp, bottom = 16.dp), ) { IconToggleButton( checked = state.accepted, @@ -120,13 +125,15 @@ fun ResetCardView( text = stringResource(id = R.string.reset_card_to_factory_warning_message), style = TangemTypography.body2, color = colorResource(id = R.color.text_secondary), + modifier = modifier + .padding(end = 20.dp), ) } - Spacer(modifier = modifier.size(32.dp)) + Spacer(modifier = modifier.size(16.dp)) Box( modifier = modifier - .padding(start = 16.dp, end = 16.dp), + .padding(start = 16.dp, end = 16.dp, bottom = 32.dp), ) { DetailsMainButton( title = stringResource(id = R.string.reset_card_to_factory_button_title), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index 8474878afc..168bd4a024 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -140,13 +140,12 @@ private fun WalletConnectSessions( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = modifier) { - Text( - text = session.description, - style = TangemTypography.subtitle1, - color = colorResource(id = R.color.text_primary_1), - ) - } + Text( + text = session.description, + style = TangemTypography.subtitle1, + color = colorResource(id = R.color.text_primary_1), + modifier = modifier.weight(1f), + ) IconButton( onClick = { state.onRemoveSession(session.sessionId) }, ) { diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt index 0a444895d7..ca82ff1fee 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt @@ -1,8 +1,17 @@ package com.tangem.tap.features.disclaimer.redux +import com.tangem.common.extensions.VoidCallback import org.rekotlin.Action -sealed class DisclaimerAction: Action { - object AcceptDisclaimer : DisclaimerAction() - object ShowAcceptedDisclaimer : DisclaimerAction() +sealed class DisclaimerAction : Action { + + data class SetDisclaimerType( + val type: DisclaimerType, + ) : DisclaimerAction() + + data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() + data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction() + + internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction() + internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 12a1cfaccd..1b42d7b4fc 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -1,28 +1,65 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.tap.common.post import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.preferencesStorage import com.tangem.tap.store +import org.rekotlin.Action import org.rekotlin.Middleware class DisclaimerMiddleware { val disclaimerMiddleware: Middleware = { dispatch, state -> { next -> { action -> - when (action) { - is DisclaimerAction.AcceptDisclaimer -> { - preferencesStorage.saveDisclaimerAccepted() - store.dispatch(NavigationAction.PopBackTo()) - - //delayed sending used for better animation - post(550) { store.dispatch(HomeAction.ReadCard) } - } - } + state()?.let { handleDisclaimerMiddleware(action, it) } next(action) } } } -} \ No newline at end of file +} + +private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { + val state = appState.disclaimerState + + when (action) { + is DisclaimerAction.SetDisclaimerType -> { + handleUpdateState = action.type.createUpdateState() + } + is DisclaimerAction.Show -> { + handleUpdateState = state.type.createUpdateState() + store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + } + is DisclaimerAction.AcceptDisclaimer -> { + action.type.accept() + handleUpdateState = action.type.createUpdateState() + store.dispatch(NavigationAction.PopBackTo()) + state.onAcceptCallback?.invoke() + store.dispatch(DisclaimerAction.SetOnAcceptCallback(null)) + } + } +} + +private fun DisclaimerType.accept() = when (this) { + DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted = true + DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted = true +} + +fun DisclaimerType.isAccepted(): Boolean = when (this) { + DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted + DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted +} + +private fun DisclaimerType.createUpdateState(): DisclaimerAction.UpdateState { + return DisclaimerAction.UpdateState(this, this.isAccepted()) +} + +private var handleUpdateState: DisclaimerAction.UpdateState = DisclaimerAction.UpdateState( + type = DisclaimerType.Tangem, + accepted = false, +) + set(value) { + field = value + store.dispatch(value) + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt index 40408ea343..1b282877f2 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt @@ -13,9 +13,15 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState { if (action !is DisclaimerAction) return state.disclaimerState val disclaimerState = state.disclaimerState + return when (action) { - is DisclaimerAction.AcceptDisclaimer, DisclaimerAction.ShowAcceptedDisclaimer -> { - disclaimerState.copy(accepted = true) - } + is DisclaimerAction.UpdateState -> disclaimerState.copy( + type = action.type, + accepted = action.accepted, + ) + is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy( + onAcceptCallback = action.onAcceptCallback, + ) + else -> disclaimerState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index b92252ae01..530a4afb78 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -1,7 +1,30 @@ package com.tangem.tap.features.disclaimer.redux +import android.net.Uri +import com.tangem.common.card.Card +import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isSaltPay import org.rekotlin.StateType data class DisclaimerState( - val accepted: Boolean = false + val accepted: Boolean = false, + val type: DisclaimerType = DisclaimerType.Tangem, + val onAcceptCallback: VoidCallback? = null, ) : StateType + +sealed class DisclaimerType( + val uri: Uri, +) { + object Tangem : DisclaimerType(Uri.parse("https://tangem.com/tangem_tos.html")) + object SaltPay : DisclaimerType(Uri.parse("https://tangem.com/soltpay_tos.html")) + + companion object { + fun get(scanResponse: ScanResponse): DisclaimerType = get(scanResponse.card) + + fun get(card: Card): DisclaimerType = when { + card.isSaltPay -> SaltPay + else -> Tangem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 4538be7224..b13e5b588b 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -6,6 +6,7 @@ import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.tap.common.extensions.configureSettings import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -16,17 +17,20 @@ import com.tangem.wallet.databinding.FragmentDisclaimerBinding import org.rekotlin.StoreSubscriber class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), - StoreSubscriber { + StoreSubscriber { private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) + activity?.onBackPressedDispatcher?.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }, + ) val inflater = TransitionInflater.from(requireContext()) enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom) exitTransition = inflater.inflateTransition(android.R.transition.slide_top) @@ -51,19 +55,25 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } - setOnClickListeners() + initViews() } - private fun setOnClickListeners() { - binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) } + private fun initViews() { + binding.webView.configureSettings() } - override fun newState(state: DisclaimerState) { + override fun newState(state: DisclaimerState) = with(binding) { if (activity == null || view == null) return if (state.accepted) { - binding.halfTransparentOverlay.hide() - binding.btnAccept.hide() + halfTransparentOverlay.hide() + btnAccept.hide() } + + btnAccept.setOnClickListener { + store.dispatch(DisclaimerAction.AcceptDisclaimer(state.type)) + } + + webView.loadUrl(state.type.uri.toString()) } } \ No newline at end of file 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..66016aef08 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,13 +6,12 @@ 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 data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction() object Init : HomeAction() - 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 e33abf03e4..ad67172568 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,31 +1,47 @@ package com.tangem.tap.features.home.redux +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.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.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.FragmentShareTransition import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerType +import com.tangem.tap.features.disclaimer.redux.isAccepted 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.SaltPayExceptionHandler +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 com.tangem.wallet.R +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.DispatchFunction import org.rekotlin.Middleware class HomeMiddleware { @@ -36,87 +52,146 @@ 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.analyticsHandler?.handleAnalyticsEvent( - 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)) +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(GlobalAction.FetchUserCountry) + } + is HomeAction.ShouldScanCardOnResume -> { + if (action.shouldScanCard) { + store.dispatch(HomeAction.ShouldScanCardOnResume(false)) + postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) } + } + } + is HomeAction.ReadCard -> { + changeButtonState(ButtonState.PROGRESS) + val scanCardAction = GlobalAction.ScanCard( + onSuccess = { + store.dispatch(HomeAction.ScanInProgress(false)) + checkForDisclaimer(it, ::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), + ) + } + } +} - 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 checkForDisclaimer(scanResponse: ScanResponse, onDisclaimerAccepted: (ScanResponse) -> Unit) { + val disclaimerType = DisclaimerType.get(scanResponse) + store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType)) + + if (disclaimerType.isAccepted()) { + onDisclaimerAccepted((scanResponse)) + } else { + changeButtonState(ButtonState.ENABLED) + store.dispatch( + DisclaimerAction.Show { + changeButtonState(ButtonState.PROGRESS) + onDisclaimerAccepted(scanResponse) + }, ) } } +private fun onScanSuccess(scanResponse: ScanResponse) { + val globalState = store.state.globalState + val tapWalletManager = globalState.tapWalletManager + tapWalletManager.updateConfigManager(scanResponse) + + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + + if (scanResponse.isSaltPay()) { + if (scanResponse.isSaltPayVisa()) { + scope.launch { + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) + delay(500) + withMainContext { + when (result) { + is Result.Success -> { + val isOnboardingCase = result.data + if (isOnboardingCase) { + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) + store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config)) + store.dispatch(OnboardingSaltPayAction.Update) + navigateTo(AppScreen.OnboardingWallet) + } else { + navigateTo(AppScreen.Wallet) + withIOContext { store.onCardScanned(scanResponse) } + } + } + is Result.Failure -> { + changeButtonState(ButtonState.ENABLED) + SaltPayExceptionHandler.handle(result.error) + } + } + } + } + } else { + if (scanResponse.card.backupStatus?.isActive == false) { + changeButtonState(ButtonState.ENABLED) + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.saltpay_error_empty_backup_title, + messageId = R.string.saltpay_error_empty_backup_message, + ), + ) + } else { + navigateTo(AppScreen.Wallet, null) + scope.launch { store.onCardScanned(scanResponse) } + } + } + } 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 7efb84deb6..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 @@ -15,16 +15,14 @@ class OnboardingHelper { fun isOnboardingCase(response: ScanResponse): Boolean { val cardInfoStorage = preferencesStorage.usedCardsPrefStorage return when { - 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 } } @@ -38,6 +36,7 @@ class OnboardingHelper { AppScreen.OnboardingOther } ProductType.Twins -> AppScreen.OnboardingTwins + ProductType.SaltPay -> AppScreen.OnboardingWallet } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt new file mode 100644 index 0000000000..2fff1d0fab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.onboarding + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class OnboardingMenuProvider : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_onboarding, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.menu_item_chat_support -> { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + true + } + else -> false + } +} \ No newline at end of file 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..199b59f22a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt @@ -0,0 +1,47 @@ +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.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.updateActivationStatus + +/** +[REDACTED_AUTHOR] + */ +class OnboardingSaltPayHelper { + companion object { + + suspend fun isOnboardingCase( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result { + return try { + val status = manager.updateActivationStatus( + amountToClaim = null, + step = SaltPayActivationStep.None, + ).successOr { return it } + + val isRegistrationCase = status.step != SaltPayActivationStep.Finished + val isBackupCase = scanResponse.card.backupStatus?.isActive == false + Result.Success(isRegistrationCase || isBackupCase) + } catch (ex: Exception) { + Result.Failure(ex) + } catch (error: SaltPayActivationError) { + Result.Failure(error) + } + } + + suspend fun testProceedToOnboarding( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(true) + + suspend fun testProceedToMainScreen( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt index f29444abc5..032cce60d3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt @@ -1,9 +1,14 @@ package com.tangem.tap.features.onboarding.products +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuProvider import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.show import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingMainBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding @@ -22,6 +27,12 @@ abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_o exitTransition = HomeToOnboardingTransition() } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) + } + + override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { lavConfetti.show(show) 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..a54a27bd28 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 @@ -1,19 +1,14 @@ package com.tangem.tap.features.onboarding.products.note -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.View import android.view.animation.OvershootInterpolator -import android.widget.ImageView import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible import androidx.transition.TransitionManager import coil.load import com.tangem.blockchain.common.Blockchain -import com.tangem.tangem_sdk_new.extensions.fadeIn -import com.tangem.tangem_sdk_new.extensions.fadeOut import com.tangem.tap.common.extensions.getDrawableCompat import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.navigation.ShareElement @@ -33,23 +28,18 @@ import com.tangem.wallet.R class OnboardingNoteFragment : BaseOnboardingFragment() { private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None + private val mainBinding by lazy { binding.vMain } private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - startPostponedEnterTransition() + mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard binding.toolbar.setTitle(R.string.onboarding_title) - btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) + btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) store.dispatch(OnboardingNoteAction.LoadCardArtwork) store.dispatch(OnboardingNoteAction.DetermineStepOfScreen) @@ -67,7 +57,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { override fun newState(state: OnboardingNoteState) { if (activity == null || view == null) return - binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { + mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) @@ -90,7 +80,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun setBalance(state: OnboardingNoteState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - with (binding.onboardingTopContainer.onboardingTvBalance) { + with(mainBinding.onboardingTopContainer.onboardingTvBalance) { if (state.balanceCriticalError == null) { val balanceValue = state.walletBalance.value.stripZeroPlainString() tvBalanceValue.text = balanceValue @@ -102,7 +92,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } } - private fun setupNoneState() = with(binding.onboardingActionContainer) { + private fun setupNoneState() = with(mainBinding.onboardingActionContainer) { btnMainAction.isVisible = false btnAlternativeAction.isVisible = false btnRefreshBalanceWidget.show(false) @@ -116,7 +106,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } private fun setupCreateWalletState(state: OnboardingNoteState) = - with(binding.onboardingActionContainer) { + with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) } btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) @@ -127,15 +117,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { tvHeader.setText(R.string.onboarding_create_wallet_header) tvBody.setText(R.string.onboarding_create_wallet_body) - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_circle) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_circle), ) updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet) btnAlternativeAction.isVisible = false // temporary } - private fun setupTopUpWalletState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { + private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { if (state.isBuyAllowed) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { @@ -163,7 +153,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { state.walletBalance.amountToCreateAccount?.let { amount -> val tvBodyMessage = getString( R.string.onboarding_top_up_body_no_account_error, - amount, state.walletBalance.currency.currencySymbol + amount, state.walletBalance.currency.currencySymbol, ) tvBody.text = tvBodyMessage } @@ -175,12 +165,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { store.dispatch(OnboardingNoteAction.Balance.Update) } } - binding.onboardingTopContainer.imvCardBackground - .setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + + val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable) updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet) } - private fun setupDoneState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { + private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.onboarding_done_button_continue) btnMainAction.setOnClickListener { showConfetti(false) @@ -188,15 +179,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } btnAlternativeAction.isVisible = false - btnAlternativeAction.setText("") + btnAlternativeAction.text = "" btnAlternativeAction.setOnClickListener { } btnRefreshBalanceWidget.mainView.setOnClickListener(null) tvHeader.setText(R.string.onboarding_done_header) tvBody.setText(R.string.onboarding_done_body) - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), ) updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation) } @@ -204,22 +195,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - with(binding.onboardingTopContainer) { + with(mainBinding.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) } } -} - -fun ImageView.swapToBitmapDrawable(bitmap: Bitmap?, duration: Long = 200) { - if (drawable is BitmapDrawable && (drawable as BitmapDrawable).bitmap == bitmap) return - - fadeOut(duration) { - setImageBitmap(bitmap) - fadeIn(duration) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 34c0d381b2..d990941bd5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -4,7 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchErrorNotification +import com.tangem.tap.common.extensions.dispatchOpenUrl +import com.tangem.tap.common.extensions.getAddressData +import com.tangem.tap.common.extensions.getToUpUrl +import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState @@ -115,11 +120,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch val balanceIsLoading = noteState.walletBalance.copy( currency = Currency.Blockchain( walletManager.wallet.blockchain, - walletManager.wallet.publicKey.derivationPath?.rawPath + walletManager.wallet.publicKey.derivationPath?.rawPath, ), state = ProgressState.Loading, error = null, - criticalError = null + criticalError = null, ) store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading)) 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..0ed23143da 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 @@ -24,17 +24,13 @@ import com.tangem.wallet.R */ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - } + private val mainBinding by lazy { binding.vMain } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - startPostponedEnterTransition() + mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard binding.toolbar.setTitle(R.string.onboarding_title) store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork) @@ -54,7 +50,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { + private val mainBinding by lazy { binding.vMain } private var previousStep = TwinCardsStep.None private lateinit var twinsWidget: TwinsCardWidget @@ -47,7 +48,6 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - postponeEnterTransition() store.dispatch(TwinCardsAction.Init) } @@ -66,30 +66,30 @@ class TwinsCardsFragment : BaseOnboardingFragment() { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - reconfigureLayoutForTwins(binding.onboardingTopContainer) + reconfigureLayoutForTwins(mainBinding.onboardingTopContainer) val typedValue = TypedValue() resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) val deviceScaleFactorForWelcomeState = typedValue.float twinsWidget = TwinsCardWidget( - LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), - deviceScaleFactorForWelcomeState + LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer), + deviceScaleFactorForWelcomeState, ) { 285f * deviceScaleFactorForWelcomeState } btnRefreshBalanceWidget = - RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) + RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) binding.toolbar.title = getText(R.string.twins_recreate_toolbar) - binding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { + mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) } - binding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { + mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { placeholder(R.drawable.card_placeholder_white) error(R.drawable.card_placeholder_white) fallback(R.drawable.card_placeholder_white) @@ -143,7 +143,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { private fun setBalance(state: TwinCardsState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - with(binding.onboardingTopContainer.onboardingTvBalance) { + with(mainBinding.onboardingTopContainer.onboardingTvBalance) { if (state.balanceCriticalError == null) { val balanceValue = state.walletBalance.value.stripZeroPlainString() tvBalanceValue.text = balanceValue @@ -167,26 +167,26 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) = - with(binding.onboardingActionContainer) { - twinsWidget.toWelcome(false) { startPostponedEnterTransition() } + with(mainBinding.onboardingActionContainer) { + twinsWidget.toWelcome(false) - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() pbBinding.pbState.hide() tvHeader.setText(R.string.twins_onboarding_subtitle) tvBody.text = getString( R.string.twins_onboarding_description_format, - state.cardNumber?.pairIndexNumber() + state.cardNumber?.pairIndexNumber(), ) btnMainAction.setText(R.string.common_continue) btnMainAction.setOnClickListener { mainAction() } } - private fun setupWarningState(state: TwinCardsState) = with(binding.onboardingActionContainer) { - twinsWidget.toWelcome(false) { startPostponedEnterTransition() } + private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + twinsWidget.toWelcome(false) - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() pbBinding.pbState.hide() chbUnderstand.show() @@ -203,198 +203,185 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } - private fun setupCreateFirstWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() - chbUnderstand.hide() - pbBinding.pbState.show() + private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() + chbUnderstand.hide() + pbBinding.pbState.show() - binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition() + mainBinding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() - when (previousStep) { - TwinCardsStep.None -> { - twinsWidget.toLeapfrog(false) { - when (state.cardNumber) { - TwinCardNumber.First -> startPostponedEnterTransition() - TwinCardNumber.Second -> { - switchToCard( - state.cardNumber, - false - ) { startPostponedEnterTransition() } - } - } - } - } - TwinCardsStep.Welcome, TwinCardsStep.Warning -> { - twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) - } - } - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchFirstStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - AndroidAssetReader(requireContext()), - ) - ) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - } - - private fun setupCreateSecondWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - switchToCard(state.cardNumber?.pairNumber()) - - val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = - getString(R.string.twins_recreate_button_format, twinPairIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchSecondStep( - Message( - getString( - R.string.twins_recreate_title_format, - twinPairIndexNumber - ) - ), - Message(getString(R.string.twins_recreate_title_preparing)), - Message(getString(R.string.twins_recreate_title_creating_wallet)), - ) - ) - } - } - - private fun setupCreateThirdWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - switchToCard(state.cardNumber) - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchThirdStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)) - ) - ) - } - } - - - private fun setupTopUpWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - when (previousStep) { - TwinCardsStep.None -> { + when (previousStep) { + TwinCardsStep.None -> { + twinsWidget.toLeapfrog(false) { when (state.cardNumber) { - TwinCardNumber.First -> { - twinsWidget.leapfrogWidget.unfold(false) { - twinsWidget.toActivate(false) { - startPostponedEnterTransition() - } - } - } + TwinCardNumber.First -> {} TwinCardNumber.Second -> { - switchToCard(state.cardNumber, false) { - twinsWidget.toActivate(false) - startPostponedEnterTransition() - } + switchToCard( + state.cardNumber, + false, + ) } } } - TwinCardsStep.CreateThirdWallet -> { - twinsWidget.toActivate() - } } - - if (state.isBuyAllowed) { - btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.TopUp) - } - - btnAlternativeAction.isVisible = true - btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) - btnAlternativeAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - } else { - btnMainAction.setText(R.string.onboarding_button_receive_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - - btnAlternativeAction.isVisible = false + TwinCardsStep.Welcome, TwinCardsStep.Warning -> { + twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) } - - tvHeader.setText(R.string.onboarding_top_up_header) - tvBody.setText(R.string.onboarding_top_up_body) - - btnRefreshBalanceWidget.changeState(state.walletBalance.state) - if (btnRefreshBalanceWidget.isShowing != true) { - btnRefreshBalanceWidget.mainView.setOnClickListener { - store.dispatch(TwinCardsAction.Balance.Update) - } - } - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) - ) - updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) } - private fun setupDoneState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - btnMainAction.setText(R.string.onboarding_done_button_continue) + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchFirstStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + AndroidAssetReader(requireContext()), + ), + ) + } + + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false + } + + private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + switchToCard(state.cardNumber?.pairNumber()) + + val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = + getString(R.string.twins_recreate_button_format, twinPairIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchSecondStep( + Message( + getString( + R.string.twins_recreate_title_format, + twinPairIndexNumber, + ), + ), + Message(getString(R.string.twins_recreate_title_preparing)), + Message(getString(R.string.twins_recreate_title_creating_wallet)), + ), + ) + } + } + + private fun setupCreateThirdWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + switchToCard(state.cardNumber) + + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchThirdStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + ), + ) + } + } + + private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + when (previousStep) { + TwinCardsStep.None -> { + when (state.cardNumber) { + TwinCardNumber.First -> { + twinsWidget.leapfrogWidget.unfold(false) { + twinsWidget.toActivate(false) + } + } + TwinCardNumber.Second -> { + switchToCard(state.cardNumber, false) { + twinsWidget.toActivate(false) + } + } + } + } + TwinCardsStep.CreateThirdWallet -> { + twinsWidget.toActivate() + } + } + + if (state.isBuyAllowed) { + btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.Confetti.Hide) - store.dispatch(TwinCardsAction.Done) + store.dispatch(TwinCardsAction.TopUp) + } + + btnAlternativeAction.isVisible = true + btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) + btnAlternativeAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + } + } else { + btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) } btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - - tvHeader.setText(R.string.onboarding_done_header) - tvBody.setText(R.string.onboarding_done_body) - - val layout = when (state.mode) { - CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done - } - updateConstraints(state.currentStep, layout) } + tvHeader.setText(R.string.onboarding_top_up_header) + tvBody.setText(R.string.onboarding_top_up_body) + + btnRefreshBalanceWidget.changeState(state.walletBalance.state) + if (btnRefreshBalanceWidget.isShowing != true) { + btnRefreshBalanceWidget.mainView.setOnClickListener { + store.dispatch(TwinCardsAction.Balance.Update) + } + } + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), + ) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) + } + + private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_done_button_continue) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.Confetti.Hide) + store.dispatch(TwinCardsAction.Done) + } + + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false + + tvHeader.setText(R.string.onboarding_done_header) + tvBody.setText(R.string.onboarding_done_body) + + val layout = when (state.mode) { + CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins + CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done + } + updateConstraints(state.currentStep, layout) + } + private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - with(binding.onboardingTopContainer) { + with(mainBinding.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) } } - private fun switchToCard( - cardNumber: TwinCardNumber?, - animate: Boolean = true, - onEnd: VoidCallback = {} - ) { + private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) { val position = cardNumber?.number ?: return if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) { @@ -405,12 +392,14 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } override fun handleOnBackPressed() { - store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction -> - store.dispatch(TwinCardsAction.Confetti.Hide) - showConfetti(false) - if (should) switchToCard(TwinCardNumber.First, true, popAction) - else popAction() - }) + store.dispatch( + TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction -> + store.dispatch(TwinCardsAction.Confetti.Hide) + showConfetti(false) + if (should) switchToCard(TwinCardNumber.First, true, popAction) + else popAction() + }, + ) } } 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 78631a152f..885f57fa99 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() - object ProceedBackup : OnboardingWalletAction() + object ResumeBackup : OnboardingWalletAction() object LoadArtwork : OnboardingWalletAction() class SetArtworkUrl(val artworkUrl: String?) : OnboardingWalletAction() @@ -28,6 +30,11 @@ sealed class BackupAction : Action { object StartAddingPrimaryCard : BackupAction() object ScanPrimaryCard : BackupAction() + /** + * Check for unfinished backup of standard Wallet cards. + * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. + * See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup + */ object CheckForUnfinishedBackup : BackupAction() object StartAddingBackupCards : BackupAction() @@ -53,15 +60,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 25fc8e2f38..148b1061a5 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 @@ -3,10 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.blockchain.common.Blockchain 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.extensions.withMainContext import com.tangem.operations.backup.BackupService -import com.tangem.tap.* +import com.tangem.tap.backupService +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -16,10 +19,19 @@ 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.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.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +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 { @@ -31,35 +43,59 @@ 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 { + when { card == null -> { - OnboardingWalletAction.ProceedBackup + // it's possible when found unfinished backup for standard Wallet cards + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> { + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + card.hasWallets() && card.backupStatus?.isActive == true -> { + when { + // check for unfinished backup for saltPay cards. See more + scanResponse.isSaltPay() && backupService.hasIncompletedBackup -> { + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + scanResponse.isSaltPay() -> { + store.dispatch(OnboardingWalletAction.GetToSaltPayStep) + store.dispatch(BackupAction.FinishBackup) + } + else -> 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.hasWallets() && card.backupStatus?.isActive == true -> - BackupAction.FinishBackup - else -> OnboardingWalletAction.GetToCreateWalletStep } - store.dispatch(action) } is OnboardingWalletAction.LoadArtwork -> { scope.launch { @@ -80,25 +116,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 @@ -110,6 +145,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) @@ -120,15 +156,19 @@ private fun handleWalletAction(action: Action) { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } - 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 + if (onboardingWalletState.isSaltPay) { + BackupAction.StartBackup + } else { + BackupAction.IntroduceBackup + } } else { null } @@ -137,16 +177,9 @@ private fun handleWalletAction(action: Action) { newAction?.let { store.dispatch(it) } } OnboardingWalletAction.OnBackPressed -> { - when (walletState.backupState.backupStep) { - BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) - BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, - BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, - -> { - store.dispatch(BackupAction.DiscardBackup) - store.dispatch(NavigationAction.PopBackTo()) - } - is BackupStep.WriteBackupCard -> - store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) + when { + onboardingWalletState.isSaltPay -> handleOnBackPressedSaltPay(onboardingWalletState) + else -> handleOnBackPressed(onboardingWalletState) } } } @@ -159,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 @@ -180,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) { @@ -212,32 +243,33 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } is BackupAction.AddBackupCard -> { + onboardingWalletState.backupCardIdFilter?.let { + backupService.skipCompatibilityChecks = true + tangemSdk.config.filter.cardIdFilter = it + } + backupService.addBackupCard { result -> + backupService.skipCompatibilityChecks = false + tangemSdk.config.filter.cardIdFilter = null when (result) { is CompletionResult.Success -> { store.dispatchOnMain(BackupAction.AddBackupCard.Success) } is CompletionResult.Failure -> { - } } } } 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)) @@ -247,22 +279,17 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) if (action.accessCodeConfirmation == backupState.accessCode) { store.dispatch(BackupAction.SetAccessCodeError(null)) backupService.setAccessCode(action.accessCodeConfirmation) + store.dispatch(OnboardingSaltPayAction.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 -> { } @@ -274,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 -> { - } } } @@ -304,17 +329,68 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.discardSavedBackup() } is BackupAction.CheckForUnfinishedBackup -> { - if (backupService.hasIncompletedBackup) { + if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPay()) { store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound)) } } - is BackupAction.ResumeBackup -> { - store.dispatch(GlobalAction.Onboarding.Start(null, true)) - store.dispatch(OnboardingWalletAction.ProceedBackup) + is BackupAction.ResumeFoundUnfinishedBackup -> { + store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } is BackupAction.DismissBackup -> { + if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException() store.dispatch(BackupAction.FinishBackup) } } +} + +private fun BackupService.primaryCardIsSaltPay(): Boolean { + return primaryCardId?.slice(0..3)?.let { + SaltPayWorkaround.isVisaBatchId(it) + } ?: false +} + +/** + * 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) + } +} + +private fun handleOnBackPressedSaltPay(state: OnboardingWalletState) { + when (state.backupState.backupStep) { + BackupStep.Finished -> { + store.dispatchDialogShow( + SaltPayDialog.Activation.TryToInterrupt( + onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onCancel = { /* do nothing */ }, + ), + ) + } + else -> handleOnBackPressed(state) + } +} + +private fun handleOnBackPressed(state: OnboardingWalletState) { + when (state.backupState.backupStep) { + BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) + BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, + BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, + -> { + store.dispatch(BackupAction.DiscardBackup) + store.dispatch(NavigationAction.PopBackTo()) + } + is BackupStep.WriteBackupCard -> + store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) + } } \ 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 ab36973e17..e90b69a31a 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,17 +36,47 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet } } +private class ReducerForGlobalAction { + companion object { + fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState { + return 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 = 2, + canSkipBackup = false, + ), + isSaltPay = false, + ) + } + 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, + maxBackupCards = if (isSaltPay) 1 else 2, + canSkipBackup = !isSaltPay, ) BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard) BackupAction.StartAddingBackupCards -> { @@ -50,9 +85,6 @@ class BackupReducer { BackupAction.AddBackupCard.Success -> { state.copy(backupCardsNumber = state.backupCardsNumber + 1) } - BackupAction.FinishAddingBackupCards -> { - state - } BackupAction.ShowAccessCodeInfoScreen -> { state.copy(backupStep = BackupStep.SetAccessCode) } @@ -62,13 +94,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) { @@ -76,12 +106,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 -> { @@ -90,43 +118,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 591573b35b..48d6ef77cf 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 @@ -1,7 +1,11 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.graphics.Bitmap +import com.tangem.common.CardFilter +import com.tangem.domain.common.SaltPayWorkaround 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.SaltPayActivationStep import org.rekotlin.StateType /** @@ -10,42 +14,69 @@ 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 { + val backupCardIdFilter: CardFilter.Companion.CardIdFilter? + get() = when { + isSaltPay -> CardFilter.Companion.CardIdFilter.Allow( + items = SaltPayWorkaround.walletCardIds.toSet(), + ranges = SaltPayWorkaround.walletCardIdRanges, + ) + else -> null + } + + 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 BackupStep.ScanOriginCard -> 3 - BackupStep.AddBackupCards -> 4 BackupStep.EnterAccessCode -> 4 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) { + SaltPayActivationStep.None, SaltPayActivationStep.NoGas -> 0 + SaltPayActivationStep.NeedPin -> 7 + SaltPayActivationStep.CardRegistration -> 8 + SaltPayActivationStep.KycIntro, SaltPayActivationStep.KycStart, + SaltPayActivationStep.KycWaiting, SaltPayActivationStep.KycReject, + -> 9 + SaltPayActivationStep.Claim -> 10 + SaltPayActivationStep.ClaimInProgress -> 11 + SaltPayActivationStep.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, @@ -72,7 +103,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..771fca7d4b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -0,0 +1,182 @@ +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.TransactionSigner +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 addressTreasureSafe = "0x8e9260a049d3Aa9ac60D0d4F27017320E0e2396B" + + 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) + } + + suspend fun getAllowance(): Result { + return walletManager.getAllowance(addressTreasureSafe, token) + } + + suspend fun transferFrom(amountToClaim: BigDecimal, signer: TransactionSigner): Result { + val amount = Amount(token, amountToClaim) + val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe) + .extractFeeAmount().successOr { return it } + + val transactionData = walletManager.createTransferFromTransaction(amount, feeAmount, addressTreasureSafe) + walletManager.transferFrom(transactionData, signer).successOr { return Result.Failure(it.error) } + + return Result.Success(Unit) + } + + 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/SaltPayActivationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt new file mode 100644 index 0000000000..d7e5ff80f7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt @@ -0,0 +1,198 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import android.net.Uri +import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.isZero +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.common.extensions.safeUpdate +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.domain.tokens.UserWalletId +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.persistence.SaltPayActivationStorage +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class SaltPayActivationManager( + val cardId: String, + val cardPublicKey: ByteArray, + val walletPublicKey: ByteArray, + private val kycProvider: KYCProvider, + private val paymentologyService: PaymentologyApiService, + private val gnosisRegistrator: GnosisRegistrator, + private val registrationStorage: SaltPayActivationStorage, +) { + val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider) + + private val approvalValue: BigDecimal = BigDecimal(2).pow(256).minus(BigDecimal.ONE) + .movePointLeft(gnosisRegistrator.walletManager.wallet.blockchain.decimals()) + + 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(SaltPayActivationError.NoGas) + } + is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayActivationError.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 SaltPayActivationError.EmptyResponse(registrationResponse.error) + } + Result.Success(registrationResponse.results[0]) + } catch (ex: SaltPayActivationError) { + 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 + } + + suspend fun getAmountToClaim(): Result { + val amount = gnosisRegistrator.getAllowance().successOr { + return Result.Failure(SaltPayActivationError.FailedToGetFundsToClaim) + } + + return if (amount.value == null || amount.value?.isZero() == true) { + Result.Failure(SaltPayActivationError.NoFundsToClaim) + } else { + Result.Success(amount) + } + } + + suspend fun claim(amountToClaim: BigDecimal, signer: TransactionSigner): Result { + val result = gnosisRegistrator.transferFrom(amountToClaim, signer).successOr { + return Result.Failure(SaltPayActivationError.ClaimTransactionFailed) + } + return Result.Success(Unit) + } + + suspend fun getTokenAmount(): Result { + val wallet = gnosisRegistrator.walletManager.safeUpdate().successOr { return it } + val token = wallet.getFirstToken().guard { + throw UnsupportedOperationException() + } + + val amountValue = wallet.amounts[AmountType.Token(token)]?.value.guard { + throw UnsupportedOperationException() + } + + return Result.Success(amountValue) + } + + 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(): SaltPayActivationManager = SaltPayActivationManager( + kycProvider = SaltPayConfig.stub().kycProvider, + paymentologyService = PaymentologyApiService.stub(), + gnosisRegistrator = GnosisRegistrator.stub(), + registrationStorage = SaltPayActivationStorage.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/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/SaltPayExceptionHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt new file mode 100644 index 0000000000..87d0c2831d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.store + +/** +[REDACTED_AUTHOR] + */ +class SaltPayExceptionHandler { + companion object { + fun handle(throwable: Throwable) { + when (throwable) { + is SaltPayActivationError -> { + val dialog = when (throwable) { + is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas + else -> SaltPayDialog.Activation.OnError(throwable) + } + store.dispatchDialogShow(dialog) + } + + is TangemSdkError -> { + when (throwable) { + is TangemSdkError.NetworkError -> { + val message = (throwable).customMessage + store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message)) + } + else -> { + // do nothing + } + } + } + else -> { + val message = throwable.message ?: "SaltPay unknown error" + store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message)) + } + } + } + } +} \ No newline at end of file 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/UtorgWebView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/UtorgWebView.kt new file mode 100644 index 0000000000..b773adcb70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/UtorgWebView.kt @@ -0,0 +1,166 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import android.text.TextUtils +import android.webkit.PermissionRequest +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.core.app.ActivityCompat +import com.tangem.tap.common.ActivityResultCallbackHolder +import com.tangem.tap.common.OnActivityResultCallback +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.isPermissionGranted +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import java.io.File + +/** +[REDACTED_AUTHOR] + */ +class UtorgWebViewClient( + private val onSuccess: () -> Unit, + private val successUrl: String, +) : WebViewClient() { + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean = + if (successUrl == request.url.toString()) { + onSuccess() + true + } else { + false + } +} + +class UtorgWebChromeClient( + private val activity: ComponentActivity, + private val activityResultCallbackHolder: ActivityResultCallbackHolder = activity as ActivityResultCallbackHolder, +) : WebChromeClient() { + + private val fileChooseRequestCode = 1002 + private val permissionRequestCode = 1003 + private val onActivityResultCallback: OnActivityResultCallback = ::onActivityResult + + private val permissionList = listOf( + "android.permission.CAMERA", + ) + + private var filePathCallback: ValueCallback>? = null + private var mediaUri: Uri? = null + + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams?, + ): Boolean { + activityResultCallbackHolder.addOnActivityResultCallback(onActivityResultCallback) + if (!readyToShowFileChooser()) return false + + val type: String + if (fileChooserParams != null && fileChooserParams.acceptTypes != null && fileChooserParams.acceptTypes.isNotEmpty()) { + type = if (!TextUtils.isEmpty(fileChooserParams.acceptTypes[0])) fileChooserParams.acceptTypes[0] else "*/*" + this.filePathCallback = filePathCallback + } else { + return false + } + proceedOnType(type, fileChooserParams.isCaptureEnabled) + + return true + } + + private fun readyToShowFileChooser(): Boolean { + val permissionsToGrant = permissionList.filterNot { activity.isPermissionGranted(it) }.toTypedArray() + return if (permissionsToGrant.isNotEmpty()) { + ActivityCompat.requestPermissions(activity, permissionsToGrant, permissionRequestCode) + false + } else { + true + } + } + + private fun proceedOnType(type: String, isCaptureEnabled: Boolean) { + if (isCaptureEnabled) { + mediaUri = Uri.fromFile(File(getImageCaptureCachePath())) + val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + imageCaptureIntent.putExtra("output", mediaUri) + activity.startActivityForResult( + Intent.createChooser(imageCaptureIntent, "Image Chooser"), + fileChooseRequestCode, + ) + } else { + mediaUri = Uri.fromFile(File(getImageCaptureCachePath())) + + val getContentIntent = Intent(Intent.ACTION_GET_CONTENT) + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE) + getContentIntent.type = "image/*" + + // val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + // imageCaptureIntent.putExtra("output", mediaUri) + + val chooserIntent = Intent.createChooser(getContentIntent, "Pick a photo") + // chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(imageCaptureIntent)) + + activity.startActivityForResult( + Intent.createChooser(chooserIntent, "Image Chooser"), + fileChooseRequestCode, + ) + } + } + + private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + fileChooseRequestCode -> { + if (resultCode != Activity.RESULT_OK) { + filePathCallback?.onReceiveValue(null) + clean() + return + } + if (data == null) { + filePathCallback?.onReceiveValue(arrayOf(mediaUri!!)) + } else { + val uri = FileChooserParams.parseResult(resultCode, data) + filePathCallback?.onReceiveValue(uri) + } + clean() + } + permissionRequestCode -> { + val notGranted = permissionList.filterNot { activity.isPermissionGranted(it) } + if (notGranted.isNotEmpty()) { + val permissionsName = notGranted.joinToString("\n") { it.permissionName() } + store.dispatchDialogShow( + AppDialog.SimpleOkDialog( + header = activity.getString(R.string.common_warning), + message = "Permission access is required to pass authorization." + + "To grant permission \n" + + "$permissionsName \n" + + "you must enable them in the app's settings", + ), + ) + } + } + } + } + + override fun onPermissionRequest(request: PermissionRequest) { + request.grant(request.resources) + } + + private fun getImageCaptureCachePath(): String = "cached_photo_" + System.currentTimeMillis() + ".jpg" + + private fun clean() { + activityResultCallbackHolder.removeOnActivityResultCallback(onActivityResultCallback) + filePathCallback = null + mediaUri = null + } + + private fun String.permissionName(): String { + return this.slice(this.lastIndexOf(".") until this.length) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt new file mode 100644 index 0000000000..579f34d614 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import android.app.Dialog +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.common.extensions.guard +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class NoFundsForActivationDialog { + companion object { + fun create(context: Context): Dialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.saltpay_error_no_gas_title) + setMessage(R.string.saltpay_error_no_gas_message) + //TODO: SaltPay: change onboarding_supplement_button_kyc_waiting -> to appropriate string + setPositiveButton(R.string.onboarding_supplement_button_kyc_waiting) { _, _ -> + val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard { + store.dispatchDebugErrorNotification("SaltPayConfig not initialized") + return@setPositiveButton + } + store.dispatch(GlobalAction.OpenChat(SupportInfo(), config)) + } + setNegativeButton(R.string.common_cancel) { _, _ -> + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt new file mode 100644 index 0000000000..831d097783 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import android.app.Dialog +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage +import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayErrorConverter +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class RegistrationErrorDialog { + companion object { + fun create(context: Context, dialog: SaltPayDialog.Activation.OnError): Dialog { + val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage + + return AlertDialog.Builder(context).apply { + setTitle(convertedMessage.title) + setMessage(convertedMessage.message) + setPositiveButton(R.string.common_ok) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt new file mode 100644 index 0000000000..fda1331a37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt @@ -0,0 +1,16 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import com.tangem.common.extensions.VoidCallback +import com.tangem.tap.common.redux.StateDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError + +/** +[REDACTED_AUTHOR] + */ +sealed class SaltPayDialog : StateDialog { + sealed class Activation : SaltPayDialog() { + object NoGas : SaltPayDialog() + data class OnError(val error: SaltPayActivationError) : SaltPayDialog() + data class TryToInterrupt(val onOk: VoidCallback, val onCancel: VoidCallback) : SaltPayDialog() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt new file mode 100644 index 0000000000..6f02df9120 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import android.app.Dialog +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class TryToInterruptRegistrationDialog { + companion object { + fun create(context: Context, dialog: SaltPayDialog.Activation.TryToInterrupt): Dialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.onboarding_exit_alert_title)) + setMessage(context.getString(R.string.onboarding_exit_alert_message)) + setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() } + setNegativeButton(R.string.common_cancel) { _, _ -> dialog.onCancel() } + setOnDismissListener { store.dispatchDialogHide() } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt new file mode 100644 index 0000000000..cc018b17e2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt @@ -0,0 +1,56 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.message + +import com.tangem.common.module.ModuleError +import com.tangem.common.module.ModuleErrorCode +import com.tangem.common.module.ModuleMessage + +sealed interface SaltPayModuleMessage : ModuleMessage + +sealed class SaltPayModuleError( + subCode: Int, + override val message: String, + override val data: Any? = null, +) : SaltPayModuleMessage, ModuleError() { + override val code: Int = ModuleErrorCode.SALT_PAY + subCode + + companion object { + // base code used for all errors in the module + internal const val ERROR_CODE_REGISTRATION = 100 +// const val CODE_ANY_OTHER = 200..299, 300..399 etc + } +} + +sealed class SaltPayActivationError( + subCode: Int, + message: String? = null, + override val data: Any? = null, +) : SaltPayModuleError( + subCode = ERROR_CODE_REGISTRATION + subCode, + message = message ?: EMPTY_MESSAGE, +) { + object Unknown : SaltPayActivationError(4) + object Empty : SaltPayActivationError(4) + + // gnosis + object FailedToMakeTxData : SaltPayActivationError(1) + object FailedToSendTx : SaltPayActivationError(2) + + object NeedPin : SaltPayActivationError(3) + object NoGas : SaltPayActivationError(5) // + class EmptyResponse(message: String?) : SaltPayActivationError(6, message) + class CardNotFound(message: String?) : SaltPayActivationError(8, message) + class CardDisabled(message: String?) : SaltPayActivationError(9, message) + class CardNotPassed(message: String?) : SaltPayActivationError(10, message) + object EmptyDynamicAttestResponse : SaltPayActivationError(11) + object EmptyBackupCardScanned : SaltPayActivationError(12) + object WeakPin : SaltPayActivationError(13) + + object FailedToGetFundsToClaim : SaltPayActivationError(14) + object NoFundsToClaim : SaltPayActivationError(15) + object ClaimTransactionFailed : SaltPayActivationError(16) + object NotClaimed : SaltPayActivationError(17) + + companion object { + const val EMPTY_MESSAGE = "" + } +} \ 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..3ab770c511 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.blockchain.common.Amount +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig +import org.rekotlin.Action +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +sealed class OnboardingSaltPayAction : Action { + sealed class Init : OnboardingSaltPayAction() { + data class SetDependencies( + val registrationManager: SaltPayActivationManager, + val saltPayConfig: SaltPayConfig, + ) : Init() + } + + data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction() + data class SetClaimRefreshInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction() + + data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction() + + object Update : OnboardingSaltPayAction() + object RegisterCard : OnboardingSaltPayAction() + object OpenUtorgKYC : OnboardingSaltPayAction() + object UtorgKYCRedirectSuccess : OnboardingSaltPayAction() + object RegisterKYC : OnboardingSaltPayAction() + object Claim : OnboardingSaltPayAction() + object RefreshClaim : OnboardingSaltPayAction() + + data class TrySetPin(val pin: String) : OnboardingSaltPayAction() + data class SetPin(val pin: String) : OnboardingSaltPayAction() + + data class SetStep(val newStep: SaltPayActivationStep? = null) : OnboardingSaltPayAction() + data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction() + data class SetTokenBalance(val balanceValue: BigDecimal) : 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..af4a354e4b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -0,0 +1,416 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.Message +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.Filter +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.KYCStatus +import com.tangem.network.api.paymentology.RegistrationResponse +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.TangemSigner +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.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdk +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.Update -> { + handleInProgress = true + + val state = getState() + scope.launch { + val updateResult = state.saltPayManager.updateActivationStatus( + amountToClaim = state.amountToClaim, + step = state.step, + ).successOr { + onException(it.error) + return@launch + } + + handleInProgress = false + withMainContext { + store.dispatch(OnboardingSaltPayAction.SetStep(updateResult.step)) + } + } + } + is OnboardingSaltPayAction.RegisterCard -> { + handleInProgress = 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(SaltPayActivationError.NeedPin) + return@launch + } + + val attestationResponse = saltPayManager.requestAttestationChallenge().successOr { + onException(it.error) + return@launch + } + + if (!attestationResponse.success) { + onError(SaltPayActivationError.CardNotFound(attestationResponse.error)) + 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), + ), + accessCode = state.accessCode, + ).successOr { + onException(it.error as Exception) + return@launch + } + + saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr { + onError(SaltPayActivationError.FailedToSendTx) + return@launch + } + + saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr { + onException(it.error) + return@launch + } + + withMainContext { + handleInProgress = false + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro)) + } + } + } + is OnboardingSaltPayAction.OpenUtorgKYC -> { + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycStart)) + } + is OnboardingSaltPayAction.UtorgKYCRedirectSuccess -> { + store.dispatch(OnboardingSaltPayAction.RegisterKYC) + } + is OnboardingSaltPayAction.RegisterKYC -> { + val state = getState() + handleInProgress = true + scope.launch { + registerKYCIfNeeded(state.saltPayManager, state.step).successOr { + onException(it.error) + return@launch + } + + handleInProgress = false + withMainContext { store.dispatch(OnboardingSaltPayAction.Update) } + } + } + is OnboardingSaltPayAction.TrySetPin -> { + try { + assertPinValid(action.pin, getState().pinLength) + store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration)) + } catch (error: SaltPayActivationError) { + SaltPayExceptionHandler.handle(error) + } + } + is OnboardingSaltPayAction.Claim -> { + val state = getState() + handleInProgress = true + + scope.launch { + val amountToClaim = getAmountToClaimIfNeeded(state.saltPayManager, state.amountToClaim).guard { + onException(SaltPayActivationError.NoFundsToClaim) + return@launch + } + + val scanResponse = store.state.globalState.scanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + val signer = TangemSigner( + card = scanResponse!!.card, + tangemSdk = tangemSdk, + initialMessage = Message(), + accessCode = state.accessCode, + ) { signResponse -> + store.dispatch( + GlobalAction.UpdateWalletSignedHashes( + walletSignedHashes = signResponse.totalSignedHashes, + walletPublicKey = state.saltPayManager.walletPublicKey, + remainingSignatures = signResponse.remainingSignatures, + ), + ) + } + state.saltPayManager.claim(amountToClaim.value!!, signer).successOr { + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim)) + onException(it.error) + return@launch + } + + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress)) + dispatchOnMain(OnboardingSaltPayAction.RefreshClaim) + } + } + is OnboardingSaltPayAction.RefreshClaim -> { + handleInProgress = true + handleClaimRefreshInProgress = true + + val state = getState() + scope.launch { + val balance = state.saltPayManager.getTokenAmount().successOr { + SaltPayExceptionHandler.handle(it.error) + handleClaimRefreshInProgress = false + return@launch + } + + handleInProgress = false + handleClaimRefreshInProgress = false + dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(balance)) + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Finished)) + } + } + else -> { + /* do nothing, only reduce */ + } + } +} + +data class UpdateResult( + val step: SaltPayActivationStep = SaltPayActivationStep.None, + val amountToClaim: Amount? = null, +) + +suspend fun SaltPayActivationManager.updateActivationStatus( + amountToClaim: Amount?, + step: SaltPayActivationStep, +): Result { + Timber.d("updateSaltPayStatus") + var updateResult = UpdateResult() + + checkGasIfNeeded(this, step).successOr { + return Result.Failure(it.error) + + } + registerKYCIfNeeded(this, step).successOr { + return Result.Failure(it.error) + + } + val saltPayStep = checkRegistration(this).successOr { + return Result.Failure(it.error) + + } + + val fetchedAmountToClaim = getAmountToClaimIfNeeded(this, amountToClaim) + updateResult = when { + // if fetchedAmountToClaim = null -> then it already claimed + saltPayStep == SaltPayActivationStep.Claim && fetchedAmountToClaim == null -> { + updateResult.copy( + step = SaltPayActivationStep.Finished, + amountToClaim = fetchedAmountToClaim, + ) + } + else -> updateResult.copy( + step = saltPayStep, + amountToClaim = fetchedAmountToClaim, + ) + } + + Timber.d("updateSaltPayStatus: success: %s", updateResult) + return Result.Success(updateResult) +} + +private suspend fun getAmountToClaimIfNeeded( + saltPayManager: SaltPayActivationManager, + amountToClaim: Amount?, +): Amount? { + Timber.d("getAmountToClaimIfNeeded: for amount: %s", amountToClaim) + if (amountToClaim != null) { + Timber.d("getAmountToClaimIfNeeded: checks no need, amount already persist") + return amountToClaim + } + + return when (val result = saltPayManager.getAmountToClaim()) { + is Result.Success -> { + Timber.d("getAmountToClaimIfNeeded: success: %s", result.data) + return result.data + } + is Result.Failure -> { + Timber.e(result.error, "getAmountToClaimIfNeeded: failure") + null + } + } +} + +private var handleClaimRefreshInProgress: Boolean = false + set(value) { + field = value + store.dispatchOnMain(OnboardingSaltPayAction.SetClaimRefreshInProgress(value)) + } + +private var handleInProgress: Boolean = false + set(value) { + field = value + store.dispatchOnMain(OnboardingSaltPayAction.SetInProgress(value)) + } + +private suspend fun onError(error: SaltPayActivationError) { + withMainContext { + handleInProgress = false + SaltPayExceptionHandler.handle(error) + } +} + +private suspend fun onException(error: Throwable) { + withMainContext { + handleInProgress = false + SaltPayExceptionHandler.handle(error) + } +} + +private suspend fun checkGasIfNeeded( + saltPayManager: SaltPayActivationManager, + step: SaltPayActivationStep, +): Result { + Timber.d("checkGasIfNeeded: for step: %s", step) + if (step == SaltPayActivationStep.KycStart || + step == SaltPayActivationStep.KycWaiting || + step == SaltPayActivationStep.Finished + ) { + Timber.d("checkGasIfNeeded: no need to check for step: %s", step) + return Result.Success(Unit) + } + + val result = saltPayManager.checkHasGas() + when (result) { + is Result.Success -> Timber.d("checkGasIfNeeded: has GAS") + is Result.Failure -> Timber.d("checkGasIfNeeded: has NO GAS") + } + return result +} + +private suspend fun registerKYCIfNeeded( + saltPayManager: SaltPayActivationManager, + step: SaltPayActivationStep, +): Result { + Timber.d("registerKYCIfNeeded: step: %s", step) + if (step != SaltPayActivationStep.KycStart) { + Timber.d("registerKYCIfNeeded: return Success, because step != KycStart") + return Result.Success(Unit) + } + + withMainContext { + Timber.d("registerKYCIfNeeded: set new step: KycWaiting") + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycWaiting)) + } + + Timber.d("saltPayManager.registerKYC()") + return saltPayManager.registerKYC() +} + +private suspend fun checkRegistration( + saltPayManager: SaltPayActivationManager, +): Result { + Timber.d("checkRegistration") + return try { + val registrationResponseItem = saltPayManager.checkRegistration().successOr { + Timber.e(it.error, "checkRegistration: failure") + return it + } + + Timber.d("checkRegistration: success: try convert to SaltPayStep") + val step = registrationResponseItem.toSaltPayStep() + Timber.d("checkRegistration: step: %s", step) + Result.Success(step) + } catch (ex: SaltPayActivationError) { + Timber.e(ex, "checkRegistration: step") + Result.Failure(ex) + } +} + +@Throws(SaltPayActivationError::class) +fun RegistrationResponse.Item.toSaltPayStep(): SaltPayActivationStep { + return when { + passed != true -> throw SaltPayActivationError.CardNotPassed(this.error) + disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(this.error) + + // go to claim screen + active == true -> SaltPayActivationStep.Claim + + // pinSet is false, go toPin screen + pinSet == false -> SaltPayActivationStep.NeedPin + + kycStatus != null -> { + when (kycStatus) { + KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycStart + KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting + KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject + KYCStatus.APPROVED -> SaltPayActivationStep.Claim + null -> throw UnsupportedOperationException() + } + } + // kycDate is set, go to kyc waiting screen + kycDate != null -> SaltPayActivationStep.KycWaiting + + else -> SaltPayActivationStep.KycIntro + } +} + +@Throws(SaltPayActivationError::class) +private fun assertPinValid(pin: String, pinLength: Int) { + val array = pin.toCharArray() + + if (array.size < pinLength) throw SaltPayActivationError.WeakPin + + val filters: List> = listOf( + AllSymbolsTheSameFilter(), + ) + + if (filters.any { it.filter(pin) }) { + throw SaltPayActivationError.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..264829c157 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt @@ -0,0 +1,78 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState +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) { + 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.SetAmountToClaim -> { + oldState.copy( + amountToClaim = action.amount, + ) + } + is OnboardingSaltPayAction.SetTokenBalance -> { + oldState.copy( + tokenAmount = oldState.tokenAmount.copy(value = action.balanceValue), + ) + } + is OnboardingSaltPayAction.SetInProgress -> { + oldState.copy( + inProgress = action.isInProgress, + ) + } + is OnboardingSaltPayAction.SetClaimRefreshInProgress -> { + oldState.copy( + claimInProgress = action.isInProgress, + ) + } + else -> 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..419c8fb750 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt @@ -0,0 +1,130 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.toHexString +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.common.SaltPayWorkaround +import com.tangem.domain.common.ScanResponse +import com.tangem.network.api.paymentology.PaymentologyApiService +import com.tangem.tap.common.toggleWidget.WidgetState +import com.tangem.tap.copyToClipboard +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.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.persistence.SaltPayActivationStorage +import com.tangem.tap.preferencesStorage +import com.tangem.tap.store +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +data class OnboardingSaltPayState( + @Transient + val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(), + val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(), + val pinCode: String? = null, + val accessCode: String? = null, + val amountToClaim: Amount? = null, + val tokenAmount: Amount = Amount(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay), BigDecimal.ZERO), + val step: SaltPayActivationStep = SaltPayActivationStep.None, + val saltPayCardArtworkUrl: String? = null, + val inProgress: Boolean = false, + val claimInProgress: Boolean = false, +) { + + val mainButtonState: WidgetState + get() = if (inProgress) ProgressState.Loading else ProgressState.Done + + fun readyToClaim(): Boolean = amountToClaim != null + + 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.saltPayActivationStorage, + kycProvider = saltPayConfig.kycProvider, + ) + test(scanResponse, gnosisRegistrator) + return registrationManager to saltPayConfig + } + + //TODO: delete + private fun test(scanResponse: ScanResponse, gnosisRegistrator: GnosisRegistrator) { + val map = mapOf( + "CID" to scanResponse.card.cardId, + "cardPublicKey" to scanResponse.card.cardPublicKey.toHexString(), + "walletPublicKey" to gnosisRegistrator.walletManager.wallet.publicKey.blockchainKey.toHexString(), + "walletAddress" to gnosisRegistrator.walletManager.wallet.address, + ) + val json = MoshiJsonConverter.INSTANCE.prettyPrint(map) + copyToClipboard(json) + } + + fun makeSaltPayRegistrationManager( + scanResponse: ScanResponse, + gnosisRegistrator: GnosisRegistrator, + paymentologyService: PaymentologyApiService, + registrationStorage: SaltPayActivationStorage, + kycProvider: KYCProvider, + ): SaltPayActivationManager { + 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 SaltPayActivationManager( + 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 SaltPayActivationStep { + None, + NoGas, + NeedPin, + CardRegistration, + KycIntro, + KycStart, + KycWaiting, + KycReject, + Claim, + ClaimInProgress, + Finished; +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt new file mode 100644 index 0000000000..6552fbd9d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt @@ -0,0 +1,377 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.ui + +import android.view.View +import android.view.ViewGroup +import android.view.animation.OvershootInterpolator +import android.widget.ProgressBar +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.children +import androidx.transition.TransitionManager +import coil.load +import coil.size.Scale +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.tangem.common.extensions.guard +import com.tangem.tap.common.compose.PinCodeWidget +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.getDrawableCompat +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.setDrawable +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.stop +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget +import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget +import com.tangem.tap.common.transitions.InternalNoteLayoutTransition +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState +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.SaltPayActivationStep +import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.store +import com.tangem.wallet.R +import com.tangem.wallet.databinding.LayoutOnboardingContainerBottomBinding +import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding +import com.tangem.wallet.databinding.LayoutOnboardingMainBinding + +/** +[REDACTED_AUTHOR] + */ +internal class OnboardingSaltPayView( + private val walletFragment: OnboardingWalletFragment, +) { + + private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar } + private val claimBinding: LayoutOnboardingMainBinding by lazy { walletFragment.bindingSaltPay.claim } + private val topContainer: LayoutOnboardingContainerTopBinding by lazy { claimBinding.onboardingTopContainer } + private val actionContainer: LayoutOnboardingContainerBottomBinding by lazy { claimBinding.onboardingActionContainer } + + private val btnRefreshBalanceWidget by lazy { + RefreshBalanceWidget(claimBinding.onboardingTopContainer.onboardingWalletContainer) + } + + private var progressButton: SaltPayProgressButton? = null + private var previousStep: SaltPayActivationStep = SaltPayActivationStep.None + + 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) { + //TODO: SaltPay: remove hardCode + if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) { + imvFrontCard.load(R.drawable.img_salt_pay_visa) { + scale(Scale.FILL) + crossfade(enable = true) + } + } else { + walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard) + } + imvFirstBackupCard.load(R.drawable.card_placeholder_wallet) + + // 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) + } + + fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) { + when (state.backupCardsNumber) { + 0 -> { + tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_no_backup_card) + tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_no_backup_cards) + } + 1 -> { + tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_one_backup_card) + tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_one_backup_card) + } + else -> {} + } + } + + 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) { + SaltPayActivationStep.NoGas -> handleNoGas() + SaltPayActivationStep.NeedPin -> handleNeedPin() + SaltPayActivationStep.CardRegistration -> handleCardRegistration() + SaltPayActivationStep.KycIntro -> handleKycIntro() + SaltPayActivationStep.KycStart -> handleKycStart(state) + SaltPayActivationStep.KycWaiting -> handleKycWaiting() + SaltPayActivationStep.KycReject -> handleKycReject() + SaltPayActivationStep.Claim -> handleClaim(state) + SaltPayActivationStep.ClaimInProgress -> handleClaim(state) + SaltPayActivationStep.Finished -> handleClaim(state) + } + progressButton?.changeState(state.mainButtonState) + } + + private fun handleNoGas() { + // normally this shouldn't happen + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchDialogShow(SaltPayDialog.Activation.NoGas) + } + + private fun handleNeedPin() = with(walletFragment.bindingSaltPay) { + toolbar.title = 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 = getString(R.string.onboarding_navbar_register_wallet) + showOnlyView(connectCard.root) { + progressButton = SaltPayProgressButton(connectCard.root) + connectCard.btnConnect.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.RegisterCard) + } + } + } + + private fun handleKycIntro() = with(walletFragment.bindingSaltPay) { + toolbar.title = getString(R.string.onboarding_navbar_kyc_start) + showOnlyView(verifyIdentity.root) { + progressButton = SaltPayProgressButton(verifyIdentity.root) + verifyIdentity.btnVerify.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.OpenUtorgKYC) + } + } + } + + private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) { + toolbar.title = 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.UtorgKYCRedirectSuccess) + }, + ) + webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity()) + webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl) + } + } + + private fun handleKycWaiting() = with(walletFragment.bindingSaltPay.kycInProgress) { + toolbar.title = getString(R.string.onboarding_navbar_kyc_progress) + showOnlyView(root) + + imvInProgress.setDrawable(R.drawable.ic_in_progress) + tvHeader.text = getString(R.string.onboarding_title_kyc_waiting) + tvBody.text = getString(R.string.onboarding_subtitle_kyc_waiting) + + btnOpenSupportChat.hide() + btnOpenSupportChat.setOnClickListener { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + btnKycAction.text = getString(R.string.onboarding_button_kyc_waiting) + btnKycAction.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Update) + } + progressButton = SaltPayProgressButton(root) + } + + private fun handleKycReject() = with(walletFragment.bindingSaltPay.kycInProgress) { + toolbar.title = getString(R.string.onboarding_navbar_kyc_progress) + showOnlyView(root) + + imvInProgress.setDrawable(R.drawable.ic_reject) + tvHeader.text = getString(R.string.onboarding_title_kyc_retry) + tvBody.text = getString(R.string.onboarding_subtitle_kyc_retry) + + btnOpenSupportChat.hide() + btnKycAction.text = getString(R.string.onboarding_button_kyc_start) + btnKycAction.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Update) + } + progressButton = SaltPayProgressButton(root) + } + + private fun handleClaim(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) { + toolbar.title = getString(R.string.onboarding_navbar_claim) + val btnMain = actionContainer.btnContainer.findViewById(R.id.btn_main_action) + val tvHeader = actionContainer.tvHeader + val tvBody = actionContainer.tvBody + + showOnlyView(claimBinding.root) { + // topContainer.progress.pbState.hide() + progressButton = SaltPayProgressButton(actionContainer.root) + topContainer.imvCardBackground.apply { + setBackgroundDrawable(context.getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + } + topContainer.imvFrontCard.load(R.drawable.img_salt_pay_visa) { + scale(Scale.FILL) + crossfade(enable = true) + } + actionContainer.btnAlternativeAction.hide() + updateConstraints(state.step, R.layout.lp_onboarding_topup_wallet) + } + + btnRefreshBalanceWidget.changeState(if (state.claimInProgress) ProgressState.Loading else ProgressState.Done) + if (btnRefreshBalanceWidget.isShowing != true) { + btnRefreshBalanceWidget.mainView.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.RefreshClaim) + } + } + val tokenAmountString = tokenAmountString(state) + topContainer.onboardingTvBalance.tvBalanceValue.text = tokenAmountString ?: "" + topContainer.onboardingTvBalance.tvBalanceCurrency.text = "" + + when (state.step) { + SaltPayActivationStep.Claim -> { + btnRefreshBalanceWidget.mainView.hide() + claimValueString(state)?.let { + tvHeader.text = getString(R.string.onboarding_title_claim, it) + } + tvBody.text = getString(R.string.onboarding_subtitle_claim) + + btnMain.text = getString(R.string.onboarding_button_claim) + btnMain.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Claim) + } + } + SaltPayActivationStep.ClaimInProgress -> { + btnRefreshBalanceWidget.mainView.show() + tvHeader.text = getString(R.string.onboarding_title_claim_progress) + tvBody.text = getString(R.string.onboarding_subtitle_claim_progress) + btnMain.text = getString(R.string.onboarding_button_claim) + } + SaltPayActivationStep.Finished -> { + tvHeader.setText(R.string.common_success) + tvBody.setText(R.string.onboarding_subtitle_success_claim) + + btnMain.setText(R.string.onboarding_button_continue_wallet) + btnMain.setOnClickListener { + walletFragment.showConfetti(false) + store.dispatch(OnboardingWalletAction.FinishOnboarding) + } + + btnRefreshBalanceWidget.mainView.setOnClickListener(null) + updateConstraints(state.step, R.layout.lp_onboarding_done_activation) + walletFragment.showConfetti(true) + } + else -> {} + } + + // need to recreate after any step + progressButton = SaltPayProgressButton(actionContainer.root) + } + + private fun tokenAmountString(state: OnboardingSaltPayState): String? { + return state.tokenAmount.value?.toFormattedCurrencyString( + decimals = state.tokenAmount.decimals, + currency = state.tokenAmount.currencySymbol, + ) + } + + private fun claimValueString(state: OnboardingSaltPayState): String? { + return state.amountToClaim?.value?.toFormattedCurrencyString( + decimals = state.amountToClaim.decimals, + currency = state.amountToClaim.currencySymbol, + ) + } + + private fun updateConstraints(currentStep: SaltPayActivationStep, @LayoutRes layoutId: Int) { + if (this.previousStep == currentStep) return + + with(claimBinding.onboardingTopContainer) { + val constraintSet = ConstraintSet() + constraintSet.clone(root.context, layoutId) + constraintSet.applyTo(onboardingWalletContainer) + val transition = InternalNoteLayoutTransition() + transition.interpolator = OvershootInterpolator() + TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) + } + } + + private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) { + walletFragment.bindingSaltPay.onboardingSaltpayContainer.children + .filter { it.id != view.id } + .forEach { it.hide() } + view.show { onShowListener?.invoke() } + } + + private fun getString(@StringRes resId: Int): String { + return walletFragment.getString(resId) + } + + private fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return walletFragment.getString(resId, *formatArgs) + } + + 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/BackupAnimator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt new file mode 100644 index 0000000000..3621bbcfa4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt @@ -0,0 +1,299 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui + +import com.google.android.material.button.MaterialButton +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.postUiDelayBg +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.FIRST_BACKUP +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.ORIGIN +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.SECOND_BACKUP +import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding + +/** +[REDACTED_AUTHOR] + */ +interface BackupAnimator { + fun updateBackupState(backupState: BackupState) + fun setupCreateWalletState() + fun showBackupIntro(state: BackupState) + fun showScanOriginCard() + fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) + fun showWritePrimaryCard(state: BackupState) + fun showWriteBackupCard(state: BackupState, backupCard: Int) + fun showSuccess(onEnd: () -> Unit) +} + +class WalletBackupAnimator( + private val cardsWidget: WalletCardsWidget, +) : BackupAnimator { + + private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) { + 2 -> State.TwoCards + 3 -> State.TreeCards + else -> throw UnsupportedOperationException() + } + + private var currentStep = -1 + + override fun updateBackupState(backupState: BackupState) { + } + + override fun setupCreateWalletState() { + currentStep = STEP_CREATE_WALLET + + cardsWidget.toFolded(false) + + ORIGIN.alpha(1f) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(1f) + } + + override fun showBackupIntro(state: BackupState) { + currentStep = STEP_BACKUP_INTRO + + ORIGIN.show() + FIRST_BACKUP.show() + SECOND_BACKUP.show() + + cardsWidget.toWelcome() + } + + override fun showScanOriginCard() { + currentStep = STEP_SCAN_ORIGIN_CARD + + cardsWidget.toFolded() + } + + override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { + when (backupCardsCounts) { + 0 -> { + when (currentStep) { + STEP_CREATE_WALLET -> { + cardsWidget.toFolded(false) { cardsWidget.toFan() } + + FIRST_BACKUP.animateAlpha(0.6f, 0) + SECOND_BACKUP.animateAlpha(0.2f, 400) + } + STEP_BACKUP_INTRO -> { + cardsWidget.toFolded(duration = 200) { cardsWidget.toFan() } + FIRST_BACKUP.animateAlpha(0.6f, 400, 250) + SECOND_BACKUP.animateAlpha(0.2f, 400, 250) + } + STEP_SCAN_ORIGIN_CARD -> { + cardsWidget.toFan() + FIRST_BACKUP.animateAlpha(0.6f, 400, 250) + SECOND_BACKUP.animateAlpha(0.2f, 400, 250) + } + } + } + 1 -> { + cardsWidget.toFan(false) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(0.2f) + } + 2 -> { + cardsWidget.toFan(false) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(1f) + } + } + + currentStep = STEP_ADD_BACKUP_CARDS + } + + override fun showWritePrimaryCard(state: BackupState) { + currentStep = STEP_WRITE_PRIMARY_CARD + + cardsWidget.toLeapfrog( + onEndFold = { + FIRST_BACKUP.alpha(0.4f) + SECOND_BACKUP.alpha(0.2f) + }, + ) + } + + override fun showWriteBackupCard(state: BackupState, backupCard: Int) { + + when (viewState) { + State.TwoCards -> { + if (currentStep == STEP_WRITE_BACKUP_CARD) return + + val delay = if (currentStep == UNDEFINED) 700L else 0 + postUiDelayBg(delay) { + FIRST_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + ORIGIN.alpha(0.4f) + } + } + } + State.TreeCards -> { + when (backupCard) { + 1 -> { + FIRST_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + SECOND_BACKUP.alpha(0.4f) + ORIGIN.alpha(0.2f) + } + } + 2 -> { + SECOND_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + ORIGIN.alpha(0.4f) + FIRST_BACKUP.alpha(0.2f) + } + } + } + } + } + currentStep = STEP_WRITE_BACKUP_CARD + } + + override fun showSuccess(onEnd: () -> Unit) { + currentStep = STEP_SUCCESS + cardsWidget.leapfrogWidget.fold(onEnd = onEnd) + } + + private enum class State { + TreeCards, + TwoCards + } + + private fun BackupCardType.animateAlpha( + alpha: Float, + duration: Long = 0L, + startDelay: Long = 0L, + ) { + when (this) { + ORIGIN -> { + cardsWidget.getOriginCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + FIRST_BACKUP -> { + cardsWidget.getFirstBackupCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + } + } + + private fun BackupCardType.alpha(alpha: Float) { + when (this) { + ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha + FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().alpha = alpha + } + } + } + + private fun BackupCardType.show() { + when (this) { + ORIGIN -> cardsWidget.getOriginCardView().show() + FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show() + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().show() + } + } + } + + companion object { + private const val UNDEFINED = -1 + private const val STEP_CREATE_WALLET = 0 + private const val STEP_BACKUP_INTRO = 1 + private const val STEP_SCAN_ORIGIN_CARD = 2 + private const val STEP_ADD_BACKUP_CARDS = 3 + private const val STEP_WRITE_PRIMARY_CARD = 4 + private const val STEP_WRITE_BACKUP_CARD = 5 + private const val STEP_SUCCESS = 6 + } +} + +class TestBackupAnimation( + private val animator: BackupAnimator, + binding: FragmentOnboardingWalletBinding, +) : BackupAnimator { + + private lateinit var state: BackupState + private var steps = 0 + + init { + init(binding, this) + } + + fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) { + steps = step + when (steps) { + 0 -> setupCreateWalletState() + 1 -> showBackupIntro(state) + 2 -> showScanOriginCard() + 3 -> showAddBackupCards(state, 0) + 4 -> showAddBackupCards(state, 1) + 5 -> showAddBackupCards(state, 2) + 6 -> showWritePrimaryCard(state) + 7 -> showWriteBackupCard(state, 1) + 8 -> showWriteBackupCard(state, 2) + else -> setStep(0) + } + onStepUpdate(steps) + } + + fun onNext(onStepUpdate: (Int) -> Unit) { + steps++ + setStep(steps, onStepUpdate) + } + + override fun updateBackupState(backupState: BackupState) { + this.state = backupState + } + + override fun setupCreateWalletState() { + animator.setupCreateWalletState() + } + + override fun showBackupIntro(state: BackupState) { + animator.showBackupIntro(state) + } + + override fun showScanOriginCard() { + animator.showScanOriginCard() + } + + override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { + animator.showAddBackupCards(state, backupCardsCounts) + } + + override fun showWritePrimaryCard(state: BackupState) { + animator.showWritePrimaryCard(state) + } + + override fun showWriteBackupCard(state: BackupState, backupCard: Int) { + animator.showWriteBackupCard(state, backupCard) + } + + override fun showSuccess(onEnd: () -> Unit) { + animator.showSuccess(onEnd) + } + + companion object { + fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) { + val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" } + button.setOnClickListener { animator.onNext { button.text = "$it" } } + binding.onboardingRoot.addView(button, 0) + } + } +} \ 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 056d5a28bb..47b6beb3aa 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 @@ -5,9 +5,10 @@ import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.widget.FrameLayout import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment +import androidx.core.view.MenuProvider import androidx.transition.TransitionInflater import androidx.transition.TransitionManager import by.kirich1409.viewbindingdelegate.viewBinding @@ -19,39 +20,42 @@ import com.tangem.common.core.CardIdDisplayFormat 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.extensions.hide +import com.tangem.tap.common.extensions.inflate import com.tangem.tap.common.extensions.show +import com.tangem.tap.features.BaseFragment import com.tangem.tap.features.FragmentOnBackPressedHandler import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState 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.ui.OnboardingSaltPayView 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), +class OnboardingWalletFragment : BaseFragment(R.layout.fragment_onboarding_wallet), StoreSubscriber, FragmentOnBackPressedHandler { - private var accessCodeDialog: AccessCodeDialog? = null + private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) + internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind) + internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer } + + private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this) + private lateinit var cardsWidget: WalletCardsWidget - private val binding: FragmentOnboardingWalletBinding by viewBinding( - FragmentOnboardingWalletBinding::bind - ) - private val pbBinding: ViewOnboardingProgressBinding by viewBinding( - ViewOnboardingProgressBinding::bind) + private var accessCodeDialog: AccessCodeDialog? = null + private lateinit var animator: BackupAnimator - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - setHasOptionsMenu(true) - + override fun configureTransitions() { val inflater = TransitionInflater.from(requireContext()) enterTransition = inflater.inflateTransition(R.transition.fade) exitTransition = inflater.inflateTransition(R.transition.fade) @@ -60,33 +64,58 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val typedValue = TypedValue() - resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) - val deviceScaleFactor = typedValue.float - - val leapfrogCalculator = PropertyCalculator( - yTranslationFactor = 25f * deviceScaleFactor, - ) - val leapfrog = LeapfrogWidget(binding.flCardsContainer, leapfrogCalculator) - cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor } - startPostponedEnterTransition() + val scaleFactor = getDeviceScaleFactor() + initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor) 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 loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() + + private fun reInitCardsWidget(backupCardsCounts: Int) = with(binding) { + val viewBackupCount = flCardsContainer.childCount - 1 + if (viewBackupCount <= 0) return@with + if (viewBackupCount == backupCardsCounts) return@with + + cardsWidget.toFolded(false) + + if (viewBackupCount > backupCardsCounts) { + flCardsContainer.removeViewAt(viewBackupCount - 1) + } else { + flCardsContainer.inflate(R.layout.view_onboarding_card, true) + } + + val scaleFactor = getDeviceScaleFactor() + initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor) + } + + private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget { + val leapfrogCalculator = PropertyCalculator(yTranslationFactor = 25f * deviceScaleFactor) + return LeapfrogWidget(container, leapfrogCalculator) + } + + private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) { + cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor) { 200f * deviceScaleFactor } + animator = if (isTest) { + TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding) + } else { + WalletBackupAnimator(cardsWidget) + } } override fun onStart() { @@ -105,30 +134,25 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun newState(state: OnboardingWalletState) { if (activity == null || view == null) return + if (state.isSaltPay) reInitCardsWidget(1) + animator.updateBackupState(state.backupState) 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) { + onboardingSaltPayView.newState(state) + } else { + loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard) + loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard) + loadImageIntoImageView(state.cardArtworkUrl, binding.imvSecondBackupCard) + handleOnboardingStep(state) } } - private fun loadImageIntoImageView(url: String?, view: ImageView) { + internal fun loadImageIntoImageView(url: String?, view: ImageView) { view.load(url) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) @@ -136,86 +160,78 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } } + internal 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) } - layoutButtonsCommon.btnAlternativeAction.hide() + layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } + layoutButtonsCommon.btnWalletAlternativeAction.hide() toolbar.title = getText(R.string.onboarding_getting_started) tvHeader.setText(R.string.onboarding_create_wallet_header) tvBody.setText(R.string.onboarding_create_wallet_body) - cardsWidget.toFolded(false) { startPostponedEnterTransition() } + animator.setupCreateWalletState() } - - 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) - is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state) - is BackupStep.WriteBackupCard -> showWriteBackupCard(state) + is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state, isSaltPay) + is BackupStep.WriteBackupCard -> showWriteBackupCard(state, isSaltPay) BackupStep.Finished -> showSuccess() } } - private fun showBackupIntro(state: BackupState) = with(binding) { + private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) { imvFirstBackupCard.show() imvSecondBackupCard.show() - cardsWidget.toWelcome() - tvHeader.hide() tvBody.hide() viewPagerBackupInfo.show() tabLayoutBackupInfo.show() 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) } - } + btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now) + btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } - startPostponedEnterTransition() + btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) + btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } + btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay) + } + animator.showBackupIntro(state) } private fun showScanOriginCard() = with(binding) { prepareBackupView() - - cardsWidget.toFolded() - tvHeader.text = getText(R.string.onboarding_title_scan_origin_card) tvBody.text = getString( R.string.onboarding_subtitle_scan_origin_card, ) with(layoutButtonsCommon) { - btnMainAction.text = getString(R.string.onboarding_button_scan_origin_card) - btnAlternativeAction.hide() - btnMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } + btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card) + btnWalletAlternativeAction.hide() + btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } } + + animator.showScanOriginCard() } - private fun prepareBackupView() = with(binding) { - toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) - - tvHeader.show() - tvBody.show() - viewPagerBackupInfo.hide() - tabLayoutBackupInfo.hide() - - imvFirstBackupCard.show() - imvSecondBackupCard.show() - } - - private fun showAddBackupCards(state: BackupState) = with(binding) { + private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) { prepareBackupView() accessCodeDialog?.dismiss() @@ -230,36 +246,43 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), layoutButtonsAddCards.btnAddCard.isEnabled = false } - when (state.backupCardsNumber) { - 0 -> { - cardsWidget.toFan() { - cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200) - cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200) - } - tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) - } - 1 -> { - tvHeader.text = getText(R.string.onboarding_title_one_backup_card) - tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) - - cardsWidget.toFan(false) - cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400) - cardsWidget.getSecondBackupCardView().alpha = 0.2f - } - 2 -> { - tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) - - cardsWidget.toFan(false) - cardsWidget.getFirstBackupCardView().alpha = 1f - cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400) - } - } - 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) { + onboardingSaltPayView.showAddBackupCards(state) + } else { + when (state.backupCardsNumber) { + 0 -> { + tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) + } + 1 -> { + tvHeader.text = getText(R.string.onboarding_title_one_backup_card) + tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) + } + 2 -> { + tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) + } + else -> {} + } + } + + animator.showAddBackupCards(state, state.backupCardsNumber) + } + + private fun prepareBackupView() = with(binding) { + toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) + + tvHeader.show() + tvBody.show() + viewPagerBackupInfo.hide() + tabLayoutBackupInfo.hide() + + imvFirstBackupCard.show() + imvSecondBackupCard.show() } private fun showSetAccessCode() { @@ -290,80 +313,71 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), accessCodeDialog?.showError(state.accessCodeError) } - private fun showWritePrimaryCard(state: BackupState) = with(binding) { + private fun showWritePrimaryCard(state: BackupState, isSaltPay: Boolean) = with(binding) { accessCodeDialog?.dismiss() + reInitCardsWidget(state.backupCardsNumber) prepareViewForFinalizeStep() - cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) - - cardsWidget.toLeapfrog() { - cardsWidget.getFirstBackupCardView().alpha = 0.6f - cardsWidget.getSecondBackupCardView().alpha = 0.2f - } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - 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) } - ) - layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin) - layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } + if (isSaltPay) { + tvHeader.text = getText(R.string.onboarding_saltpay_title_prepare_origin) + tvBody.text = getString(R.string.onboarding_twins_interrupt_warning) + layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_saltpay_button_backup_origin) + } else { + 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) }, + ) + layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin) + } + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } + animator.showWritePrimaryCard(state) } private fun prepareViewForFinalizeStep() = with(binding) { - layoutButtonsAddCards.root.hide() - layoutButtonsCommon.root.show() - - imvFirstBackupCard.show() - imvSecondBackupCard.show() - toolbar.title = getText(R.string.onboarding_button_finalize_backup) + layoutButtonsAddCards.root.hide() + layoutButtonsCommon.root.show() + layoutButtonsCommon.btnWalletAlternativeAction.hide() + imvCardBackground.hide() - - cardsWidget.toLeapfrog() - - layoutButtonsCommon.btnAlternativeAction.hide() + imvFirstBackupCard.show() + imvSecondBackupCard.show() } - private fun showWriteBackupCard(state: BackupState) = with(binding) { + private fun showWriteBackupCard(state: BackupState, isSaltPay: Boolean) = with(binding) { prepareViewForFinalizeStep() - cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) - val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1 - when (cardNumber) { - 1 -> cardsWidget.leapfrogWidget.leap() { - cardsWidget.getOriginCardView().alpha = 0.4f - cardsWidget.getFirstBackupCardView().alpha = 0.2f - cardsWidget.getSecondBackupCardView().alpha = 1f - } - 2 -> cardsWidget.leapfrogWidget.leap() { - cardsWidget.getOriginCardView().alpha = 0.4f - cardsWidget.getFirstBackupCardView().alpha = 0.2f - cardsWidget.getSecondBackupCardView().alpha = 1f - } + if (isSaltPay) { + tvHeader.text = getString(R.string.onboarding_saltpay_title_backup_card) + tvBody.text = getString(R.string.onboarding_twins_interrupt_warning) + layoutButtonsCommon.btnWalletMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card) + } else { + val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) + 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]), + ) + layoutButtonsCommon.btnWalletMainAction.text = getString( + R.string.onboarding_button_backup_card_format, + cardNumber, + ) } - - val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - 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]) - ) - layoutButtonsCommon.btnMainAction.text = getString( - R.string.onboarding_button_backup_card_format, - cardNumber - ) - layoutButtonsCommon.btnMainAction.setOnClickListener { + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } + + animator.showWriteBackupCard(state, cardNumber) } - private fun showSuccess() = with(binding) { + internal fun showSuccess() = with(binding) { + toolbar.title = getString(R.string.onboarding_done_header) tvHeader.text = getText(R.string.onboarding_done_header) tvHeader.show() @@ -372,29 +386,40 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), tabLayoutBackupInfo.hide() tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) - layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_continue_wallet) - layoutButtonsCommon.btnAlternativeAction.hide() - layoutButtonsCommon.btnMainAction.setOnClickListener { - vConfetti.lavConfetti.cancelAnimation() - vConfetti.lavConfetti.hide() + layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet) + layoutButtonsCommon.btnWalletAlternativeAction.hide() + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { + showConfetti(false) store.dispatch(OnboardingWalletAction.FinishOnboarding) } - cardsWidget.leapfrogWidget.fold { + animator.showSuccess { flCardsContainer.hide() imvCardBackground.hide() - vConfetti.lavConfetti.show() - vConfetti.lavConfetti.playAnimation() + showConfetti(true) imvSuccess.alpha = 0f imvSuccess.show() + imvSuccess.animate()?.alpha(1f)?.duration = 400 + } + } - imvSuccess.animate() - ?.alpha(1f) - ?.setDuration(400) + internal fun showConfetti(show: Boolean) = with(binding.vConfetti) { + lavConfetti.show(show) + + if (show) { + lavConfetti.playAnimation() + } else { + lavConfetti.cancelAnimation() } } override fun handleOnBackPressed() { store.dispatch(OnboardingWalletAction.OnBackPressed) } -} \ No newline at end of file + + private fun getDeviceScaleFactor(): Float { + val typedValue = TypedValue() + resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) + return typedValue.float + } +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt index 9f42b3be0b..7d4831c434 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt @@ -16,82 +16,100 @@ class WalletCardsWidget( val getTopOfAnchorViewForActivateState: () -> Float, ) { - var currentState: WidgetState? = null + private val animDuration: Long = 400 - fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) { + var currentState: WidgetState? = null + private set + + fun toWelcome( + animate: Boolean = true, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.WELCOME) return currentState = WidgetState.WELCOME - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createWelcomeProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createWelcomeProperties(BackupCardType.SECOND_BACKUP)) - ) + val animator = createAnimator(animate, onEnd = onEnd) + animator.playTogether(createAnimators(::createWelcomeProperties)) leapfrogWidget.fold { animator.start() } } - fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toFolded( + animate: Boolean = true, + duration: Long = animDuration, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.FOLDED) return currentState = WidgetState.FOLDED - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)) - ) - leapfrogWidget.fold(animate) { animator.start() } + val animator = createAnimator(animate, duration) { + leapfrogWidget.fold(animate, onEnd) + } + animator.playTogether(createAnimators(::createLeapfrogProperties)) + animator.start() } - fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toFan( + animate: Boolean = true, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.FAN) return currentState = WidgetState.FAN - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createFanProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createFanProperties(BackupCardType.SECOND_BACKUP)) - ) + val animator = createAnimator(animate, onEnd = onEnd) + animator.playTogether(createAnimators(::createFanProperties)) leapfrogWidget.fold { animator.start() } } - fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toLeapfrog( + animate: Boolean = true, + onEndFold: () -> Unit = {}, + onEnd: () -> Unit = {}, + ) { + if (currentState == WidgetState.LEAPFROG) return + currentState = WidgetState.LEAPFROG - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)) - ) - leapfrogWidget.fold { - animator.doOnEnd { + val onAnimatorEnd = { + leapfrogWidget.fold(animate) { leapfrogWidget.initViews() - leapfrogWidget.unfold() + onEndFold() + leapfrogWidget.unfold(animate, onEnd) } - animator.start() } + val animator = createAnimator(animate, onEnd = onAnimatorEnd) + animator.playTogether(createAnimators(::createLeapfrogProperties)) + animator.start() } - private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet { + private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List { + return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) } + } + + private fun getCardTypesTypes(): List = when (leapfrogWidget.getViewsCount()) { + 2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP) + 3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP) + else -> throw UnsupportedOperationException() + } + + private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet { return AnimatorSet().apply { - duration = if (animate) 400 else 0 + this.duration = if (animate) duration else 0 doOnEnd { onEnd() } } } - private fun createAnimator( + private fun createCardAnimator( cardType: BackupCardType, properties: CardProperties, ): ObjectAnimator { - val view = getLeapViewByCardNumber(cardType).view - val animator = ObjectAnimator.ofPropertyValuesHolder(view, - *properties.createValuesHolders().toTypedArray()) + val view = getLeapViewByCard(cardType).view + val animator = ObjectAnimator.ofPropertyValuesHolder( + view, + *properties.createValuesHolders().toTypedArray(), + ) view.elevation = properties.elevation return animator } @@ -176,7 +194,7 @@ class WalletCardsWidget( private fun createActivateProperties(cardType: BackupCardType): CardProperties { val topOfAnchorView = getTopOfAnchorViewForActivateState() - val twinProperties = CardProperties.from(getLeapViewByCardNumber(cardType).state) + val twinProperties = CardProperties.from(getLeapViewByCard(cardType).state) return twinProperties.copy( xTranslation = twinProperties.xTranslation, yTranslation = twinProperties.yTranslation - topOfAnchorView, @@ -185,33 +203,47 @@ class WalletCardsWidget( ) } - private fun getLeapViewByCardNumber(cardType: BackupCardType): LeapView { - return when (cardType) { - BackupCardType.ORIGIN -> leapfrogWidget.getViewByPosition(0) - BackupCardType.FIRST_BACKUP -> leapfrogWidget.getViewByPosition(1) - BackupCardType.SECOND_BACKUP -> leapfrogWidget.getViewByPosition(2) - } - } - + @Throws(UnsupportedOperationException::class) fun getOriginCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.ORIGIN).view as ImageView + return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView } + @Throws(UnsupportedOperationException::class) fun getFirstBackupCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.FIRST_BACKUP).view as ImageView + return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView } + @Throws(UnsupportedOperationException::class) fun getSecondBackupCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView + return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView + } + + @Throws(UnsupportedOperationException::class) + private fun getLeapViewByCard(cardType: BackupCardType): LeapView { + fun getByIndex(index: Int): LeapView { + return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index)) + } + + return when (leapfrogWidget.getViewsCount()) { + 2 -> when (cardType) { + BackupCardType.ORIGIN -> getByIndex(1) + BackupCardType.FIRST_BACKUP -> getByIndex(0) + BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException() + } + 3 -> when (cardType) { + BackupCardType.ORIGIN -> getByIndex(2) + BackupCardType.FIRST_BACKUP -> getByIndex(1) + BackupCardType.SECOND_BACKUP -> getByIndex(0) + } + else -> throw UnsupportedOperationException() + } } enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG } } - enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP } - private data class CardProperties( val xTranslation: Float = 0f, val yTranslation: Float = 0f, 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/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 4e57c0ea39..3890f95d40 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -245,8 +245,7 @@ private fun sendTransaction( } is SimpleResult.Failure -> { store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - wallet = walletManager.wallet, - host = walletManager.currentHost, + walletManager = walletManager, amountToSend = amountToSend, feeAmount = feeAmount, destinationAddress = destinationAddress, 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/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 96f742ec97..9817b4db49 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -278,7 +278,7 @@ class TokensMiddleware { val factory = store.state.globalState.tapWalletManager.walletManagerFactory val derivationStyle = scanResponse.card.derivationStyle - val addActions = currencyList.mapNotNull { currency -> + val addActions = currencyList.mapIndexedNotNull { index, currency -> when (currency) { is Currency.Blockchain -> { val derivationPath = currency.derivationPath?.let { DerivationPath(it) } @@ -293,12 +293,12 @@ class TokensMiddleware { scanResponse = scanResponse, blockchain = currency.blockchain, derivationParams = derivationParams, - ) ?: return@mapNotNull null + ) ?: return@mapIndexedNotNull null val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) WalletAction.MultiWallet.AddBlockchain( blockchain = blockchainNetwork, walletManager = walletManager, - save = true, + save = index == currencyList.lastIndex, ) } is Currency.Token -> { @@ -309,7 +309,7 @@ class TokensMiddleware { WalletAction.MultiWallet.AddToken( token = currency.token, blockchain = blockchainNetwork, - save = true, + save = index == currencyList.lastIndex, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index 8abbc33a2b..9a9052115c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -30,10 +30,14 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentAddTokensBinding import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.StoreSubscriber - class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), StoreSubscriber { @@ -123,7 +127,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.popular_tokens, menu) + inflater.inflate(R.menu.menu_popular_tokens, menu) val addCustomTokenAllowed = store.state.tokensState.allowToAdd menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt index 1500a10a1e..06e3cbae64 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -59,9 +59,9 @@ sealed interface Currency { fun isToken(): Boolean = this is Token fun toTokenResponse(): TokenResponse { return TokenResponse( - id = coinId ?: "", + id = coinId, networkId = blockchain.toNetworkId(), - derivationPath = derivationPath ?: DERIVATION_PATH_RAW_VALUE, + derivationPath = derivationPath, name = currencyName, symbol = currencySymbol, decimals = decimals, @@ -70,7 +70,6 @@ sealed interface Currency { } companion object { - private const val DERIVATION_PATH_RAW_VALUE = "m/44/0'/0/0" fun fromBlockchainNetwork( blockchainNetwork: BlockchainNetwork, token: com.tangem.blockchain.common.Token? = null, @@ -111,12 +110,9 @@ sealed interface Currency { ) } - fun fromTokenResponse(tokenResponse: TokenResponse): Currency { - val derivationPath = if (tokenResponse.derivationPath == DERIVATION_PATH_RAW_VALUE) { - null - } else { - tokenResponse.derivationPath - } + fun fromTokenResponse(tokenResponse: TokenResponse): Currency? { + val blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId) + ?: return null return when { tokenResponse.contractAddress != null -> Token( com.tangem.blockchain.common.Token( @@ -126,12 +122,12 @@ sealed interface Currency { decimals = tokenResponse.decimals, id = tokenResponse.id, ), - blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!, - derivationPath = derivationPath, + blockchain = blockchain, + derivationPath = tokenResponse.derivationPath, ) else -> Blockchain( - blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!, - derivationPath = derivationPath, + blockchain = blockchain, + derivationPath = tokenResponse.derivationPath, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt index eb6c8caf49..3b80a95845 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt @@ -5,8 +5,7 @@ sealed class WalletWarning( ) { data class ExistentialDeposit( val currencyName: String, - val currencySymbols: String, - val existentialDepositString: String, + val edStringValueWithSymbol: String, ) : WalletWarning(1) object TransactionInProgress : WalletWarning(10) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index ed617762f4..836d287739 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -39,7 +39,7 @@ sealed class WalletAction : Action { data class NoAccount( val wallet: Wallet, val blockchain: BlockchainNetwork, - val amountToCreateAccount: String + val amountToCreateAccount: String, ) : WalletAction() data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() @@ -47,6 +47,10 @@ sealed class WalletAction : Action { data class SetArtworkId(val artworkId: String?) : WalletAction() + sealed class UserTokens : WalletAction() { + object Loading : UserTokens() + object Loaded : UserTokens() + } sealed class MultiWallet : WalletAction() { data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index d6f0def028..6219fea552 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -45,6 +45,7 @@ data class WalletState( val totalBalance: TotalBalance? = null, val showBackupWarning: Boolean = false, val missingDerivations: List = emptyList(), + val loadingUserTokens: Boolean = false, ) : StateType { // if you do not delegate - the application crashes on startup, @@ -192,21 +193,28 @@ data class WalletState( fun removeWallet(walletData: WalletData?): WalletState { if (walletData == null) return this - return if (walletData.currency is Currency.Blockchain) { - val walletStores = wallets.filterNot { - it.blockchainNetwork.blockchain == walletData.currency.blockchain - && it.blockchainNetwork.derivationPath == walletData.currency.derivationPath + return when (val currency = walletData.currency) { + is Currency.Blockchain -> { + val walletStores = wallets.filterNot { + it.blockchainNetwork.blockchain == currency.blockchain + && it.blockchainNetwork.derivationPath == currency.derivationPath + } + copy(wallets = walletStores) + .updateTotalBalance() + .updateProgressState() + } + is Currency.Token -> { + val walletStore = getWalletStore(walletData.currency) + val walletDataList = walletStore?.walletsData + ?.filterNot { it.currency == walletData.currency } + ?: emptyList() + val updatedWalletManager = walletStore?.walletManager?.also { it.removeToken(currency.token) } + val updatedWalletStore = walletStore?.copy( + walletsData = walletDataList, + walletManager = updatedWalletManager, + ) + updateWalletStore(updatedWalletStore) } - copy(wallets = walletStores) - .updateTotalBalance() - .updateProgressState() - } else { - val walletStore = getWalletStore(walletData.currency) - val walletDataList = walletStore?.walletsData - ?.filterNot { it.currency == walletData.currency } - ?: emptyList() - val updatedWalletStore = walletStore?.copy(walletsData = walletDataList) - updateWalletStore(updatedWalletStore) } } @@ -352,8 +360,7 @@ data class WalletData( if (existentialDepositString != null) { val warning = WalletWarning.ExistentialDeposit( currencyName = currency.currencyName, - currencySymbols = currency.currencySymbol, - existentialDepositString = existentialDepositString, + edStringValueWithSymbol = "$existentialDepositString ${currency.currencySymbol}", ) walletWarnings.add(warning) } 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/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 2825983575..52a199ff60 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -135,7 +135,19 @@ class WalletMiddleware { .plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath)) } action.coinsList != null -> action.coinsList - else -> walletState.walletsData.map { it.currency } + else -> { + if (walletState.isMultiwalletAllowed) { + walletState.walletsData.map { it.currency } + } else { + val derivationPath = walletState.primaryWallet?.currency?.derivationPath + val primaryBlockchain = walletState.primaryBlockchain + val primaryToken = walletState.primaryToken + listOfNotNull( + primaryBlockchain?.let { Currency.Blockchain(it, derivationPath) }, + primaryToken?.let { Currency.Token(it, primaryBlockchain!!, derivationPath) }, + ) + } + } } val ratesResult = globalState.tapWalletManager.rates.loadFiatRate( currencyId = appCurrencyId, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index 52ab9c4d75..892b5db149 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -65,7 +65,7 @@ class MultiWalletReducer { } val selectedCurrency = if (!state.isMultiwalletAllowed) { - wallets[0].walletsData[0].currency + wallets.firstOrNull()?.walletsData?.firstOrNull()?.currency } else { state.selectedCurrency } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index 63d378718f..9f2f76e6fd 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -21,6 +21,7 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.store +import java.math.BigDecimal class OnWalletLoadedReducer { @@ -115,6 +116,8 @@ class OnWalletLoadedReducer { private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState { if (wallet.blockchain != walletState.primaryBlockchain) return walletState + // val ratesRespository = store.state.globalState.tapWalletManager.ratesRepository + // val tokenCurrency = val fiatCurrencyName = store.state.globalState.appCurrency.code val token = wallet.getFirstToken() val tokenData = if (token != null) { @@ -124,10 +127,12 @@ class OnWalletLoadedReducer { val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencyName) } TokenData( - tokenAmount.value?.toFormattedCurrencyString( - token.decimals, token.symbol + amount = tokenAmount.value ?: BigDecimal.ZERO, + tokenSymbol = tokenAmount.currencySymbol, fiatAmountFormatted = tokenFiatAmount, + fiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatValue(tokenFiatRate) }, + amountFormatted = tokenAmount.value?.toFormattedCurrencyString( + token.decimals, token.symbol, ) ?: "", - tokenAmount.currencySymbol, tokenFiatAmount ) } else { null diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 79aa56c3df..3a180040a3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -31,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import org.rekotlin.Action +import timber.log.Timber import java.math.BigDecimal class WalletReducer { @@ -314,6 +315,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState { is WalletAction.AppCurrencyAction -> { newState = appCurrencyReducer.reduce(action, newState) } + is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true) + is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false) else -> { /* no-op */ } } @@ -387,16 +390,11 @@ private fun setNewFiatRate( state = state ) } else { - val fiatRate = fiatRates.entries.firstOrNull() - val currency = fiatRate?.key ?: return state - val rate = fiatRate.value ?: return state - - setSingleWalletFiatRate( - rate = rate, - rateFormatted = rateFormatter(rate), - currency = currency, + setSingleWalletFiatRates( + fiatRates = fiatRates.mapNotNullValues { it.value }, + rateFormatter = rateFormatter, appCurrency = appCurrency, - state = state + state = state, ) } } @@ -427,10 +425,10 @@ private fun setMultiWalletFiatRate( walletData.copy( currencyData = currencyData.copy( fiatAmountFormatted = fiatAmountFormatted, - fiatAmount = fiatAmount + fiatAmount = fiatAmount, ), fiatRate = rate, - fiatRateString = rateFormatter(rate) + fiatRateString = rateFormatter(rate), ) } @@ -438,15 +436,51 @@ private fun setMultiWalletFiatRate( .updateWalletsData(newWalletsData) } +private fun setSingleWalletFiatRates( + fiatRates: Map, + appCurrency: FiatCurrency, + rateFormatter: (BigDecimal) -> String, + state: WalletState, +): WalletState { + val blockchainFiatRate = fiatRates.entries.firstOrNull { it.key.isBlockchain() } + val tokenFiatRate = fiatRates.entries.firstOrNull { it.key.isToken() } + Timber.e("Token Fiat Rate is ${tokenFiatRate ?: "NULL"}") + val updatedState = updateStateWithFiatRate(blockchainFiatRate?.toPair(), appCurrency, rateFormatter, state) + return updateStateWithFiatRate(tokenFiatRate?.toPair(), appCurrency, rateFormatter, updatedState) +} + +private fun updateStateWithFiatRate( + fiatRate: Pair?, + appCurrency: FiatCurrency, + rateFormatter: (BigDecimal) -> String, + state: WalletState, +): WalletState { + return if (fiatRate != null) { + val currency = fiatRate.first + val rate = fiatRate.second + + setSingleWalletFiatRate( + rate = rate, + rateFormatted = rateFormatter(rate), + currency = currency, + appCurrency = appCurrency, + state = state, + ) + } else { + state + } +} + private fun setSingleWalletFiatRate( rate: BigDecimal, rateFormatted: String, currency: Currency, appCurrency: FiatCurrency, - state: WalletState + state: WalletState, ): WalletState { val wallet = state.primaryWalletManager?.wallet ?: return state val token = wallet.getFirstToken() + Timber.e("Working with currency: ${currency.currencyName}") if (currency == state.primaryWallet?.currency) { val fiatAmount = wallet.amounts[AmountType.Coin]?.value @@ -457,23 +491,38 @@ private fun setSingleWalletFiatRate( fiatRateString = rateFormatted ) return state.updateWalletData(walletData) + } else if (currency is Currency.Token && currency.token == token) { + Timber.e("Working with token fiat rate") + val tokenFiatAmount = wallet.getTokenAmount(token) ?.value - ?.toFiatString(rate, appCurrency.code) + val tokenAmountFormatted = tokenFiatAmount?.toFiatString(rate, appCurrency.code) + val tokenData = state.primaryWallet?.currencyData?.token?.copy( + fiatAmountFormatted = tokenAmountFormatted, fiatAmount = tokenFiatAmount, fiatRate = rate, - fiatRateString = rateFormatted + fiatRateString = rateFormatted, ) + // ?: TokenData( + // fiatAmountFormatted = tokenAmountFormatted, + // fiatAmount = tokenFiatAmount, + // fiatRate = rate, + // fiatRateString = rateFormatted, + // amount = "", + // tokenSymbol = currency.currencySymbol + // ) + + Timber.e("Token Data is ${tokenData ?: "NULL"}") val walletData = state.primaryWallet?.copy( currencyData = state.primaryWallet.currencyData.copy( - token = tokenData - ) + token = tokenData, + ), ) - val wallets = walletData?.let { listOf(walletData) } ?: emptyList() - return state.updateWalletsData(wallets) + Timber.e("Wallet Data is ${walletData ?: "NULL"}") + return state.updateWalletData(walletData) } return state } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index c627aa2e32..34ca1f10c1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -34,9 +34,11 @@ data class BalanceWidgetData( ) data class TokenData( - val amount: String, + val amountFormatted: String, + val amount: BigDecimal? = null, val tokenSymbol: String, - val fiatAmount: String? = null, + val fiatAmountFormatted: String? = null, + val fiatAmount: BigDecimal? = null, val fiatRateString: String? = null, val fiatRate: BigDecimal? = null, ) @@ -151,11 +153,11 @@ class BalanceWidget( groupBaseCurrency.show() tvCurrency.text = data.token?.tokenSymbol tvBaseCurrency.text = data.currency - tvAmount.text = if (showAmount) data.token?.amount else "" + tvAmount.text = if (showAmount) data.token?.amountFormatted else "" tvBaseAmount.text = if (showAmount) data.amountFormatted else "" if (showAmount) { tvFiatAmount.show() - tvFiatAmount.text = data.token?.fiatAmount + tvFiatAmount.text = data.token?.fiatAmountFormatted } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index db9173e486..281d4b3938 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -42,7 +42,7 @@ import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter import com.tangem.tap.features.wallet.ui.images.load -import com.tangem.tap.features.wallet.ui.test.TestWalletDetails +import com.tangem.tap.features.wallet.ui.test.TestWallet import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletDetailsBinding @@ -125,7 +125,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), view?.findViewById(R.id.l_balance)?.let { view -> TestActions.initFor( view = view, - actions = TestWalletDetails.solanaRentExemptWarning() + actions = TestWallet.solanaRentExemptWarning() ) } } @@ -345,7 +345,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.wallet_details, menu) + inflater.inflate(R.menu.menu_wallet_details, menu) } private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) { 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 1389600f94..87a31563fa 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 @@ -26,7 +26,6 @@ import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printWalletState import com.tangem.tap.domain.termsOfUse.CardTou import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -42,7 +41,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 @@ -54,11 +52,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + !state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> { walletView = SingleWalletView() walletView.changeWalletView(this, binding) } @@ -139,11 +139,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) store.state.globalState.scanResponse?.let { scanNoteResponse -> - store.dispatch(DetailsAction.PrepareScreen( - scanNoteResponse, - store.state.walletState.walletManagers.map { it.wallet }, - CardTou(), - )) + store.dispatch( + DetailsAction.PrepareScreen( + scanNoteResponse, + store.state.walletState.walletManagers.map { it.wallet }, + CardTou(), + ), + ) store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) true } @@ -206,7 +219,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { context.getString( R.string.warning_existential_deposit_message, - message.currencyName, message.currencySymbols, message.existentialDepositString, + message.currencyName, message.edStringValueWithSymbol, ) } is WalletWarning.BalanceNotEnoughForFee -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index af061d48f1..a688bb9e42 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -110,8 +110,12 @@ class WalletAdapter lContent.tvExchangeRate.text = wallet.fiatRateString ?: root.getString(id = R.string.token_item_no_rate) - cardWallet.setOnClickListener { - store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + if (wallet.walletAddresses != null) { + cardWallet.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + } + } else { + cardWallet.setOnClickListener(null) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt index 3fc5187741..d52c26763e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt @@ -36,6 +36,23 @@ class SimpleOkDialog { } }.create() } - } + fun create(dialog: AppDialog.SimpleOkErrorDialog, context: Context): AlertDialog = create( + dialog = AppDialog.SimpleOkDialog( + header = context.getString(R.string.common_error), + message = dialog.message, + onOk = dialog.onOk, + ), + context = context, + ) + + fun create(dialog: AppDialog.SimpleOkWarningDialog, context: Context): AlertDialog = create( + dialog = AppDialog.SimpleOkDialog( + header = context.getString(R.string.common_warning), + message = dialog.message, + onOk = dialog.onOk, + ), + context = context, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt index 6b99ef8be9..4f384d33f7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt @@ -1,9 +1,13 @@ package com.tangem.tap.features.wallet.ui.test import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager +import com.tangem.common.extensions.guard import com.tangem.tap.common.TestAction import com.tangem.tap.common.TestActions +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store @@ -13,7 +17,7 @@ import kotlin.random.Random /** [REDACTED_AUTHOR] */ -class TestWalletDetails { +class TestWallet { companion object { fun solanaRentExemptWarning(): List { val checker = SolanaRentWarningActionEmitter() @@ -26,6 +30,80 @@ class TestWalletDetails { "BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() }, ) } + + fun getBlockchainBalanceActions(blockchainNetwork: BlockchainNetwork): List { + return getBlockchainBalanceActions( + getWalletManager(blockchainNetwork), + blockchainNetwork.blockchain.decimals(), + ) + } + + fun getTokenBalanceAction(blockchainNetwork: BlockchainNetwork, token: Token): List { + return getTokenBalanceAction(getWalletManager(blockchainNetwork), token) + } + + fun getBlockchainBalanceActions(walletManager: WalletManager?, decimals: Int): List { + val minValue = BigDecimal.ONE.movePointLeft(decimals) + val averageValue = BigDecimal.ONE.movePointRight(decimals) + .divide(BigDecimal(2)).movePointLeft(decimals) + val maxValue = BigDecimal(2).pow(32) + + return listOf( + "0.0" to { setBalance(walletManager, BigDecimal.ZERO) }, + "Min value" to { setBalance(walletManager, minValue) }, + "Average value" to { setBalance(walletManager, averageValue) }, + "Max value" to { setBalance(walletManager, maxValue) }, + "1234567890123.01234567890123" to { setBalance(walletManager, BigDecimal.ZERO) }, + ) + } + + fun getTokenBalanceAction(walletManager: WalletManager?, token: Token): List { + val minValue = BigDecimal.ONE.movePointLeft(token.decimals) + val averageValue = BigDecimal.ONE.movePointRight(token.decimals) + .divide(BigDecimal(2)).movePointLeft(token.decimals) + val maxValue = BigDecimal(2).pow(32) + + return listOf( + "0.0" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) }, + "Min value" to { setTokenBalance(walletManager, minValue, token) }, + "Average value" to { setTokenBalance(walletManager, averageValue, token) }, + "Max value" to { setTokenBalance(walletManager, maxValue, token) }, + "1234567890123.01234567890123" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) }, + ) + } + + fun setBalance(walletManager: WalletManager?, value: BigDecimal) { + val manager = walletManager.guard { + store.dispatchDebugErrorNotification("WalletManager not found") + return + } + val amount = manager.wallet.amounts[AmountType.Coin] ?: Amount(manager.wallet.blockchain) + setBalance(manager, amount, value) + } + + private fun setTokenBalance(walletManager: WalletManager?, value: BigDecimal, token: Token? = null) { + val manager = walletManager.guard { + store.dispatchDebugErrorNotification("WalletManager not found") + return + } + val token = token.guard { + store.dispatchDebugErrorNotification("Token not found") + return + } + + val amount = manager.wallet.amounts[AmountType.Token(token)] ?: Amount(token) + setBalance(manager, amount, value) + } + + private fun setBalance(walletManager: WalletManager, amount: Amount, value: BigDecimal) { + TestActions.testAmountInjectionForWalletManagerEnabled = true + walletManager.wallet.setAmount(amount.copy(value = value)) + store.dispatch(WalletAction.LoadData) + } + + private fun getWalletManager(blockchainNetwork: BlockchainNetwork): WalletManager? { + return store.state.walletState.getWalletManager(blockchainNetwork) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 9015ba4fc4..9d4fe68894 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -81,6 +81,8 @@ class MultiWalletView : WalletView() { handleRescanWarning(binding, state.missingDerivations.isNotEmpty()) walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken) + binding.pbLoadingUserTokens.show(state.loadingUserTokens) + binding.btnAddToken.setOnClickListener { val card = store.state.globalState.scanResponse!!.card store.dispatch( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt index 95509f5b37..49fcb89c16 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt @@ -1,12 +1,15 @@ package com.tangem.tap.features.wallet.ui.wallet +import com.tangem.domain.common.extensions.debounce import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.animateVisibility import com.tangem.tap.common.extensions.formatAmountAsSpannedString import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.databinding.LayoutSingleWalletBalanceBinding +import org.rekotlin.Action import java.math.BigDecimal data class SaltPayBalanceWidgetData( @@ -26,7 +29,7 @@ class SaltPayBalanceWidget( veilBalance.veil() veilBalanceCrypto.veil() } else { - veilBalance.unVeil() + // veilBalance.unVeil() veilBalanceCrypto.unVeil() } tvProcessing.animateVisibility( @@ -35,9 +38,18 @@ class SaltPayBalanceWidget( veilBalanceCrypto.animateVisibility( show = data.state != ProgressState.Error, ) - tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString( - currencySymbol = data.fiatCurrency?.symbol ?: "", - ) + if (data.fiatAmount == null) { + //TODO: SaltPay: A tricky solution to the problem with displaying rates + // If the rates are loaded after the walletManager.update() is completely updated, + // then this problem can be avoided + actionDebouncer(WalletAction.LoadFiatRate()) + } else { + veilBalance.unVeil() + tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString( + currencySymbol = data.fiatCurrency?.symbol ?: "", + ) + } + tvBalanceCrypto.text = data.currency tvCurrencyName.text = data.fiatCurrency?.code @@ -48,4 +60,5 @@ class SaltPayBalanceWidget( } } +private val actionDebouncer = debounce(500, mainScope) { store.dispatch(it) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt index 09d00b294e..e765e738e8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt @@ -2,11 +2,12 @@ package com.tangem.tap.features.wallet.ui.wallet import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.store import com.tangem.wallet.databinding.FragmentWalletBinding +import timber.log.Timber class SaltPaySingleWalletView : WalletView() { override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { @@ -18,8 +19,10 @@ class SaltPaySingleWalletView : WalletView() { private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { rvMultiwallet.hide() btnAddToken.hide() + rowButtons.hide() rvPendingTransaction.hide() tvTwinCardNumber.hide() + pbLoadingUserTokens.hide() lCardBalance.root.hide() lAddress.root.hide() lSingleWalletBalance.root.show() @@ -30,20 +33,21 @@ class SaltPaySingleWalletView : WalletView() { override fun onNewState(state: WalletState) { val binding = binding ?: return - state.primaryWallet ?: return + val tokenData = state.primaryWallet?.currencyData?.token ?: return - setupBalance(state, state.primaryWallet, binding) + setupBalance(state, tokenData, binding) } - private fun setupBalance(state: WalletState, primaryWallet: WalletData, binding: FragmentWalletBinding) { + private fun setupBalance(state: WalletState, tokenData: TokenData, binding: FragmentWalletBinding) { binding.lSingleWalletBalance.root.show() + Timber.e("Current address is ${state.primaryWalletManager?.wallet?.address}") SaltPayBalanceWidget( binding = binding.lSingleWalletBalance, data = SaltPayBalanceWidgetData( state = state.state, - currencySymbol = primaryWallet.currencyData.currencySymbol, - currency = primaryWallet.currencyData.amountFormatted, - fiatAmount = primaryWallet.currencyData.fiatAmount, + currencySymbol = tokenData.tokenSymbol, + currency = tokenData.amountFormatted, + fiatAmount = tokenData.fiatAmount, // TODO: show fiatAmount fiatCurrency = store.state.globalState.appCurrency, ), ).setup() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 9b8a2e759f..a0126391a3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.fitChipsByGroupWidth +import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState @@ -36,6 +37,7 @@ class SingleWalletView : WalletView() { rvMultiwallet.hide() btnAddToken.hide() rvPendingTransaction.hide() + pbLoadingUserTokens.hide() lCardBalance.root.show() lAddress.root.show() lSingleWalletBalance.root.hide() @@ -85,11 +87,7 @@ class SingleWalletView : WalletView() { private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) { twinCardsState?.cardNumber?.let { cardNumber -> tvTwinCardNumber.show() - val number = when (cardNumber) { - TwinCardNumber.First -> 1 - TwinCardNumber.Second -> 2 - } - tvTwinCardNumber.text = fragment?.getString(R.string.wallet_twins_chip_format, number, 2) + tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, 2) } if (twinCardsState?.cardNumber == null) { tvTwinCardNumber.hide() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index dec1f05bae..35cc636808 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -76,7 +76,12 @@ class MercuryoService( if (!isBuyAllowed()) return false // blockchains which cant be defined by mercuryo service - val unsupportedBlockchains = listOf(Blockchain.Unknown, Blockchain.Binance, Blockchain.Arbitrum) + val unsupportedBlockchains = listOf( + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.Arbitrum, + Blockchain.Optimism, + ) val blockchain = currency.blockchain return when (currency) { diff --git a/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt new file mode 100644 index 0000000000..7c016eaa5d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.persistence + +import android.content.SharedPreferences +import androidx.core.content.edit + +/** +[REDACTED_AUTHOR] + */ +class DisclaimerPrefStorage( + private val preferences: SharedPreferences, +) { + + var hasTangemTosAccepted: Boolean + get() = preferences.getBoolean(KEY_TANGEM_TOS, false) + set(value) = preferences.edit { putBoolean(KEY_TANGEM_TOS, value) } + + var hasSaltPayTosAccepted: Boolean + get() = preferences.getBoolean(KEY_SALT_PAY_TOS, false) + set(value) = preferences.edit { putBoolean(KEY_SALT_PAY_TOS, value) } + + companion object { + private const val KEY_TANGEM_TOS = "tangem_tos_accepted" + private const val KEY_SALT_PAY_TOS = "saltPay_tos_accepted" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index aeab14497b..1fad689559 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -7,14 +7,16 @@ import androidx.core.content.edit import com.tangem.common.json.MoshiJsonConverter import java.util.* - class PreferencesStorage(applicationContext: Application) { - private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + private val preferences: SharedPreferences = + applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) val appRatingLaunchObserver: AppRatingLaunchObserver val usedCardsPrefStorage: UsedCardsPrefStorage val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage + val saltPayActivationStorage: SaltPayActivationStorage + val disclaimerPrefStorage: DisclaimerPrefStorage init { incrementLaunchCounter() @@ -23,6 +25,8 @@ class PreferencesStorage(applicationContext: Application) { usedCardsPrefStorage.migrate() fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE) fiatCurrenciesPrefStorage.migrate() + saltPayActivationStorage = SaltPayActivationPrefStorage(applicationContext, MoshiJsonConverter.INSTANCE) + disclaimerPrefStorage = DisclaimerPrefStorage(preferences) } var chatFirstLaunchTime: Long? @@ -36,14 +40,6 @@ class PreferencesStorage(applicationContext: Application) { return usedCardsPrefStorage.wasScanned(cardId) } - fun saveDisclaimerAccepted() { - preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) } - } - - fun wasDisclaimerAccepted(): Boolean { - return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false) - } - fun saveTwinsOnboardingShown() { preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) } } @@ -59,12 +55,10 @@ class PreferencesStorage(applicationContext: Application) { companion object { private const val PREFERENCES_NAME = "tapPrefs" - private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted" private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown" private const val APP_LAUNCH_COUNT_KEY = "launchCount" private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" } - } class AppRatingLaunchObserver( diff --git a/app/src/main/java/com/tangem/tap/persistence/SaltPayActivationPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/SaltPayActivationPrefStorage.kt new file mode 100644 index 0000000000..9bfbddc594 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/persistence/SaltPayActivationPrefStorage.kt @@ -0,0 +1,82 @@ +package com.tangem.tap.persistence + +import android.content.Context +import androidx.core.content.edit +import at.favre.lib.armadillo.ArmadilloSharedPreferences +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.common.services.secure.SecureStorage +import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences + +/** +[REDACTED_AUTHOR] + */ +interface SaltPayActivationStorage { + var data: SaltPayActivationData + + fun reset() + + companion object { + fun stub(): SaltPayActivationStorage = object : SaltPayActivationStorage { + override var data: SaltPayActivationData = SaltPayActivationData() + override fun reset() {} + } + } +} + +data class SaltPayActivationData( + val transactionsSent: Boolean = false, +) + +class SaltPayActivationPrefStorage( + context: Context, + private val converter: MoshiJsonConverter, +) : SaltPayActivationStorage { + + private val storage: ArmadilloSharedPreferences = SecureStorage.createEncryptedSharedPreferences( + context = context, + storageName = "saltpay", + ) + + private var isFetching: Boolean = false + + init { + fetch() + } + + override var data: SaltPayActivationData = SaltPayActivationData() + set(value) { + field = value + if (!isFetching) save() + } + + override fun reset() { + storage.edit(true) { + this.remove(REGISTRATION_DATA_KEY) + data = SaltPayActivationData() + } + } + + private fun save() { + storage.edit(true) { + putString(REGISTRATION_DATA_KEY, converter.toJson(data)) + } + } + + private fun fetch(): SaltPayActivationData { + isFetching = true + + val jsonData = storage.getString(REGISTRATION_DATA_KEY, null) + data = if (jsonData == null) { + SaltPayActivationData() + } else { + converter.fromJson(jsonData) ?: SaltPayActivationData() + } + 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_ethereumfair_no_color.xml b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml new file mode 100644 index 0000000000..dd68efa83e --- /dev/null +++ b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ethereumfair_round.xml b/app/src/main/res/drawable/ic_ethereumfair_round.xml new file mode 100644 index 0000000000..b8e6604a7a --- /dev/null +++ b/app/src/main/res/drawable/ic_ethereumfair_round.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml new file mode 100644 index 0000000000..aa369fd0ab --- /dev/null +++ b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ethereumpow_round.xml b/app/src/main/res/drawable/ic_ethereumpow_round.xml new file mode 100644 index 0000000000..052ac58398 --- /dev/null +++ b/app/src/main/res/drawable/ic_ethereumpow_round.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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_reject.xml b/app/src/main/res/drawable/ic_reject.xml new file mode 100644 index 0000000000..776aa18595 --- /dev/null +++ b/app/src/main/res/drawable/ic_reject.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_scan_card.xml b/app/src/main/res/drawable/ic_scan_card.xml index e30ad93bea..d190690243 100644 --- a/app/src/main/res/drawable/ic_scan_card.xml +++ b/app/src/main/res/drawable/ic_scan_card.xml @@ -1,21 +1,31 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + - - - - - + android:pathData="M12,7m-1.25,0a1.25,1.25 0,1 1,2.5 0a1.25,1.25 0,1 1,-2.5 0" + android:fillColor="#000000"/> + + + + + 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/ic_tangem_warning.xml b/app/src/main/res/drawable/ic_tangem_warning.xml new file mode 100644 index 0000000000..aa1e0c73ee --- /dev/null +++ b/app/src/main/res/drawable/ic_tangem_warning.xml @@ -0,0 +1,24 @@ + + + + + + + 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..02b199c1e5 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-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index e635e3ee6f..b14f8571de 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -13,7 +13,7 @@ android:layout_marginBottom="16dp" android:gravity="center" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -39,7 +39,7 @@ android:layout_marginTop="10dp" android:layout_marginEnd="64dp" android:minLines="3" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_header" @@ -55,14 +55,14 @@ android:textStyle="bold" android:visibility="gone" app:buttonTint="@color/accent" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/linearLayoutCompat" tools:visibility="visible" /> + tools:text="@string/home_button_scan" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_tokens.xml b/app/src/main/res/layout/fragment_add_tokens.xml index a845918beb..f7a8fa24dc 100644 --- a/app/src/main/res/layout/fragment_add_tokens.xml +++ b/app/src/main/res/layout/fragment_add_tokens.xml @@ -19,7 +19,7 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - app:menu="@menu/popular_tokens" + app:menu="@menu/menu_popular_tokens" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title="@string/add_tokens_title" /> diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index bec0ceace0..604410e507 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -22,33 +22,34 @@ android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title="@string/disclaimer_title" /> + + android:layout_height="match_parent" + android:layout_marginStart="16dp" + android:layout_marginEnd="24dp" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingTop="16dp" + android:paddingBottom="120dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + @@ -66,8 +67,8 @@ android:layout_height="100dp" android:background="@drawable/bg_half_transparent_overlay" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toEndOf="@+id/guideline" /> diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml index faa2c0c829..c88f910dcb 100644 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ b/app/src/main/res/layout/fragment_onboarding_main.xml @@ -12,7 +12,7 @@ - - - - - - - + @@ -28,16 +28,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipChildren="false"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="match_parent" + android:visibility="gone" /> - - - - - - + @@ -60,7 +60,6 @@ android:adjustViewBounds="true" android:contentDescription="@null" android:elevation="3dp" - android:maxHeight="215dp" android:scaleType="fitCenter" app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/card_placeholder_black" /> @@ -191,11 +190,31 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" android:nestedScrollingEnabled="false" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" /> + + + + diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml index 04649efe6b..1467361b47 100644 --- a/app/src/main/res/layout/fragment_wallet_details.xml +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:theme="@style/ThemeOverlay.MyTheme.Toolbar" - app:menu="@menu/wallet_details" + app:menu="@menu/menu_wallet_details" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title=" " /> diff --git a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml index 1de5055e97..2d9ddedad7 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml @@ -1,32 +1,29 @@ + 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..f51dffd3cb 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -6,7 +6,7 @@ android:layout_height="wrap_content"> + tools:text="@string/home_button_scan" /> diff --git a/app/src/main/res/layout/layout_onboarding_main.xml b/app/src/main/res/layout/layout_onboarding_main.xml new file mode 100644 index 0000000000..29b34f187f --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_main.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file 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..fbb3534b9b --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + \ 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_kyc_in_progress.xml b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml new file mode 100644 index 0000000000..580995fc6f --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_kyc_start.xml b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_start.xml new file mode 100644 index 0000000000..f1d03e8347 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_start.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ 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_single_wallet_balance.xml b/app/src/main/res/layout/layout_single_wallet_balance.xml index 119baed8cf..984d73bf24 100644 --- a/app/src/main/res/layout/layout_single_wallet_balance.xml +++ b/app/src/main/res/layout/layout_single_wallet_balance.xml @@ -4,15 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/card_balance" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + app:cardCornerRadius="12dp"> @@ -50,8 +52,8 @@ android:layout_height="wrap_content" android:maxLines="1" android:minWidth="152dp" - android:textColor="@color/darkGray6" - android:textSize="26sp" + android:textColor="@color/text_primary_1" + android:textSize="24sp" android:textStyle="bold" tools:text="22 325.40 $" /> @@ -104,10 +106,12 @@ android:id="@+id/tv_currency_name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/darkGray1" - android:textSize="16sp" + android:gravity="center_vertical" + android:textColor="@color/text_tertiary" + android:textSize="14sp" + android:textStyle="bold" app:drawableEndCompat="@drawable/ic_arrow_angle_down" - app:drawableTint="@color/darkGray1" + app:drawableTint="@color/text_tertiary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="USD" /> diff --git a/app/src/main/res/layout/layout_wallet_rescan_warning.xml b/app/src/main/res/layout/layout_wallet_rescan_warning.xml index 8d8e2f1233..c795c2c22a 100644 --- a/app/src/main/res/layout/layout_wallet_rescan_warning.xml +++ b/app/src/main/res/layout/layout_wallet_rescan_warning.xml @@ -8,9 +8,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingStart="16dp" - android:paddingTop="14dp" + android:paddingTop="12dp" android:paddingEnd="16dp" - android:paddingBottom="14dp"> + android:paddingBottom="12dp"> @@ -33,8 +33,8 @@ android:layout_marginStart="8dp" android:layout_marginEnd="16dp" android:text="@string/main_scan_card_warning_view_title" - android:textColor="@color/textBlack" - android:textSize="14sp" + android:textColor="@color/text_primary_1" + android:textSize="16sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@id/tv_rescan_warning_subtitle" app:layout_constraintEnd_toStartOf="@id/iv_rescan_warning_end_icon" @@ -47,8 +47,8 @@ android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="@string/main_scan_card_warning_view_subtitle" - android:textColor="@color/iconGray" - android:textSize="12sp" + android:textColor="@color/text_secondary" + android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/tv_rescan_warning_title" app:layout_constraintStart_toStartOf="@id/tv_rescan_warning_title" 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/app/src/main/res/layout/view_onboarding_card.xml b/app/src/main/res/layout/view_onboarding_card.xml new file mode 100644 index 0000000000..e3086ff628 --- /dev/null +++ b/app/src/main/res/layout/view_onboarding_card.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_onboarding.xml b/app/src/main/res/menu/menu_onboarding.xml new file mode 100644 index 0000000000..bc09d08fae --- /dev/null +++ b/app/src/main/res/menu/menu_onboarding.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/menu_popular_tokens.xml similarity index 100% rename from app/src/main/res/menu/popular_tokens.xml rename to app/src/main/res/menu/menu_popular_tokens.xml diff --git a/app/src/main/res/menu/wallet.xml b/app/src/main/res/menu/menu_wallet.xml similarity index 100% rename from app/src/main/res/menu/wallet.xml rename to app/src/main/res/menu/menu_wallet.xml diff --git a/app/src/main/res/menu/wallet_details.xml b/app/src/main/res/menu/menu_wallet_details.xml similarity index 100% rename from app/src/main/res/menu/wallet_details.xml rename to app/src/main/res/menu/menu_wallet_details.xml diff --git a/app/src/main/res/values-de/strings_blockchain.xml b/app/src/main/res/values-de/strings_blockchain.xml index 5cb1360d71..66998f875e 100644 --- a/app/src/main/res/values-de/strings_blockchain.xml +++ b/app/src/main/res/values-de/strings_blockchain.xml @@ -18,10 +18,6 @@ Der Gesamtbetrag geht über die Bilanz hinaus Die Gebühr geht über die Bilanz hinaus Minimum balance is %s - Memo - Tag - Invalid Memo. It won\'t be added to the transaction - Invalid Tag. It won\'t be added to the transaction - %s network has a concept of Existential Deposit. If your account drops below %s %s it will be deactivated and any remaining funds will be destroyed. + %s network has a concept of Existential Deposit. If your account drops below %s it will be deactivated and any remaining funds will be destroyed. Destination account is not active. Send %s or more to activate the account. \ No newline at end of file diff --git a/app/src/main/res/values-de/strings_final.xml b/app/src/main/res/values-de/strings_final.xml index f32a97ae89..384c389dbe 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -48,6 +48,10 @@ Notice Attention The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -88,4 +101,56 @@ Select network Scan your card To access all the networks you need to scan the card + Failed to establish WalletConnect session + Memo + Tag + Invalid Memo. It won\'t be added to the transaction. + Invalid Tag. It won\'t be added to the transaction. + Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify via Utorg + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings_blockchain.xml b/app/src/main/res/values-fr/strings_blockchain.xml index 5bbb85c2f3..197f42551c 100644 --- a/app/src/main/res/values-fr/strings_blockchain.xml +++ b/app/src/main/res/values-fr/strings_blockchain.xml @@ -18,10 +18,6 @@ Réduire de %s XTZ Non, envoyer toute la somme Minimum balance is %s - Memo - Tag - Invalid Memo. It won\'t be added to the transaction - Invalid Tag. It won\'t be added to the transaction - %s network has a concept of Existential Deposit. If your account drops below %s %s it will be deactivated and any remaining funds will be destroyed. + %s network has a concept of Existential Deposit. If your account drops below %s it will be deactivated and any remaining funds will be destroyed. Destination account is not active. Send %s or more to activate the account. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings_final.xml b/app/src/main/res/values-fr/strings_final.xml index ef550053ca..384c389dbe 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -45,9 +45,13 @@ Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom - The server is not available, please try again later Notice Attention + The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -88,4 +101,56 @@ Select network Scan your card To access all the networks you need to scan the card + Failed to establish WalletConnect session + Memo + Tag + Invalid Memo. It won\'t be added to the transaction. + Invalid Tag. It won\'t be added to the transaction. + Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify via Utorg + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings_blockchain.xml b/app/src/main/res/values-it/strings_blockchain.xml index 775778dbb0..92df7b9703 100644 --- a/app/src/main/res/values-it/strings_blockchain.xml +++ b/app/src/main/res/values-it/strings_blockchain.xml @@ -18,10 +18,6 @@ Riduci di %s XTZ No, invia l\'intero importo Minimum balance is %s - Memo - Tag - Invalid Memo. It won\'t be added to the transaction - Invalid Tag. It won\'t be added to the transaction - %s network has a concept of Existential Deposit. If your account drops below %s %s it will be deactivated and any remaining funds will be destroyed. + %s network has a concept of Existential Deposit. If your account drops below %s it will be deactivated and any remaining funds will be destroyed. Destination account is not active. Send %s or more to activate the account. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings_final.xml b/app/src/main/res/values-it/strings_final.xml index ef550053ca..384c389dbe 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -45,9 +45,13 @@ Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom - The server is not available, please try again later Notice Attention + The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -88,4 +101,56 @@ Select network Scan your card To access all the networks you need to scan the card + Failed to establish WalletConnect session + Memo + Tag + Invalid Memo. It won\'t be added to the transaction. + Invalid Tag. It won\'t be added to the transaction. + Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify via Utorg + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ca3e7e93f1..41ae2c46a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -129,7 +129,6 @@ Tangem Twin Один кошелек. Две карты. Одна, которую Вы держите в руках, и вторая карта с номером #%s - Карта %1d из %2d Пересоздать Twin кошелек Условия использования карты Tangem Twin @@ -243,7 +242,7 @@ Баланс Отправляйте только %s (%s) на этот адрес. Иначе это может привести к утрате средств. Отправляйте только %s (%s) из сети %s на этот адрес. Иначе это может привести к утрате средств. - Если процесс повторного создания кошелька каким-либо образом прервется, вам придется начать все сначала. + Если процесс создания кошелька каким-либо образом прервется, вам придется начать все сначала. Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. Внутренняя ошибка: не удается создать менеджер кошельков Блокчейн недоступен. Попробуй позже. @@ -296,7 +295,7 @@ Все резервные карты могут использоваться как полнофункциональные с одинаковыми ключами Вы сможете установить код доступа для защиты своих кошельков Код доступа можно восстановить с помощью одной из резервных карт - Ваша карта Tangem Wallet настроена и готова к использованию + Ваша карта настроена и готова к использованию. Вы создали 1 резервную карту, и теперь эта карта готова к использованию Основная карта Я понимаю diff --git a/app/src/main/res/values-ru/strings_blockchain.xml b/app/src/main/res/values-ru/strings_blockchain.xml index 61a4c91467..9ccdfd276e 100644 --- a/app/src/main/res/values-ru/strings_blockchain.xml +++ b/app/src/main/res/values-ru/strings_blockchain.xml @@ -18,5 +18,6 @@ Уменьшить на %s XTZ Нет, отправить все Минимальный баланс: %s + Сеть %s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %s, он будет деактивирован, а все оставшиеся средства будут уничтожены. Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index b1ffa0ba10..bcf57b6db6 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -97,11 +97,58 @@ Сброс к заводским настройкам Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать эту карту для восстановления кода доступа. Выберите сеть + Отсканируйте карту + Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту Memo Tag Недопустимый Memo. Он не будет добавлен в транзакцию. Недопустимый Tag. Он не будет добавлен в транзакцию. - Сеть %s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %s %s, он будет деактивирован, а все оставшиеся средства будут уничтожены. - Отсканируйте карту - Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту + Не удалось установить сессию WalletConnect + Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Для начала работы с картой сначала отсканируйте первую карту и сделайте бэкап + Внимание + Приложите карту с логотипом Visa + Недостаточно средств для активации + Пожалуйста обратитесь в службу поддержки + Ввод одинаковых цифр является не безопасным + Данный Код доступа может быть легко взломан + Код доступа + Подключиться + Верификация клиента + Подтвердите свою личность + В обработке + Установить Код доступа + Зарегистрироваться + Верифицировать (Utorg) + Обновить + Подключите свою карту + Подтвердите свою личность + Подтверждение личности в процессе + Подключите вашу карту к децентрализованной платежной системе + Для начала работы с картой вам необходимо завершить процесс подтверждения личности + Пожалуйста дождитесь завершения процесса подтверждения вашей личности. Как правило, это занимает меньше часа. Вы можете закрыть приложение и вернуться позже. + Код доступа + Установите Код доступа для вашей SaltPay карты + Чат поддержки + Данная карта не подходит для бэкапа SaltPay + Пожалуйста, удерживайте карту до завершения операции + Для начала процесса бэкапа вам необходимо добавить Tangem карту + Бэкап карта не добавлена + Бэкап карта создана + Завершите процесс бэкапа создав код доступа + Приготовьте SaltPay карту + Приложите SaltPay карту + Приложите Tangem карту + Чат + Начало работы + Запросить %s + Для начала работы просто запросите начисление wxDai на свой кошелек + Запросить + Поздравляем! Ваша платежная крипто карта теперь активирована! + Запрашивается + Это займет несколько секунд + Что-то пошло не так + Более подробная информация отправлена на ваш адрес электронной почты. + Вы хотите выйти из процесса активации? + В этом случае вам будет необходимо начать процесс заново. diff --git a/app/src/main/res/values-v29/zendesk_styles.xml b/app/src/main/res/values-v29/zendesk_styles.xml index 628e83211a..764f632e87 100644 --- a/app/src/main/res/values-v29/zendesk_styles.xml +++ b/app/src/main/res/values-v29/zendesk_styles.xml @@ -1,7 +1,6 @@ - - diff --git a/app/src/main/res/values/strings_blockchain.xml b/app/src/main/res/values/strings_blockchain.xml index 6a496314b1..8014c2a199 100644 --- a/app/src/main/res/values/strings_blockchain.xml +++ b/app/src/main/res/values/strings_blockchain.xml @@ -18,10 +18,6 @@ Reduce by %s XTZ No, send all Minimum balance is %s - Memo - Tag - Invalid Memo. It won\'t be added to the transaction - Invalid Tag. It won\'t be added to the transaction - %s network has a concept of Existential Deposit. If your account drops below %s %s it will be deactivated and any remaining funds will be destroyed. + %s network has a concept of Existential Deposit. If your account drops below %s it will be deactivated and any remaining funds will be destroyed. Destination account is not active. Send %s or more to activate the account. \ No newline at end of file diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index 4585bee0c1..76dc5aa8dc 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -101,4 +101,56 @@ Select network Scan your card To access all the networks you need to scan the card + Failed to establish WalletConnect session + Memo + Tag + Invalid Memo. It won\'t be added to the transaction. + Invalid Tag. It won\'t be added to the transaction. + Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify via Utorg + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index afdc44412f..153f80c666 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -16,8 +16,6 @@ One wallet. Two cards. This one that you are holding in your hands and the other one with number #%s. - Card %1d of %2d - Re-create twin wallet Card terms of use @@ -201,7 +199,7 @@ Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds. Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds. - If the process of re-creating the wallet gets interrupted in any way, you\'ll have to start over. + If the process of creating the wallet gets interrupted in any way, you\'ll have to start over. The twinning process is partly complete. You can\'t exit it now. Internal error: can\'t create wallet manager @@ -267,7 +265,7 @@ You will be able to set an access code to protect your wallets. Access code can be restored with one of backup cards. - Your Tangem Wallet card is configured and ready for use. + Your wallet card is configured and ready for use. You have created 1 backup card, and now this card are ready for use. Primary Card I understand diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 09cdff0b35..dad17e5962 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -70,6 +70,20 @@ 2dp + + + + + + diff --git a/common/src/main/java/com/tangem/common/Filter.kt b/common/src/main/java/com/tangem/common/Filter.kt new file mode 100644 index 0000000000..6607f4080f --- /dev/null +++ b/common/src/main/java/com/tangem/common/Filter.kt @@ -0,0 +1,8 @@ +package com.tangem.common + +/** +[REDACTED_AUTHOR] + */ +interface Filter { + fun filter(value: T): Boolean +} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt new file mode 100644 index 0000000000..11da28c037 --- /dev/null +++ b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt @@ -0,0 +1,13 @@ +package com.tangem.common.module + +/** +[REDACTED_AUTHOR] + */ +object ModuleErrorCode { + const val APP = 100000 + + const val COMMON = 10000 + const val NETWORK = 20000 + const val DOMAIN = 30000 + const val SALT_PAY = 40000 +} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleException.kt b/common/src/main/java/com/tangem/common/module/ModuleException.kt deleted file mode 100644 index 233d47f942..0000000000 --- a/common/src/main/java/com/tangem/common/module/ModuleException.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.common.module - -/** -[REDACTED_AUTHOR] - * A module exception - */ -interface ModuleException { - val message: String -} - -/** - * An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue. - */ -interface FbConsumeException \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt index 240e7c8759..c8632c9597 100644 --- a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt +++ b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt @@ -6,17 +6,22 @@ package com.tangem.common.module */ interface ModuleMessage -interface ModuleMessageConverter { - fun convert(message: ModuleMessage): R -} - /** * @property code describes what feature is the error coming from * @property message the error description * @property data any data that can help in the part where this error is being handled */ -interface ModuleError : ModuleMessage { - val code: Int - val message: String - val data: Any? +abstract class ModuleError() : Throwable(), ModuleMessage { + abstract val code: Int + abstract override val message: String + abstract val data: Any? +} + +/** + * An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue. + */ +interface FbConsumeException + +interface ModuleMessageConverter { + fun convert(message: ModuleMessage): R } \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 5d1d0d6fa5..a7b0e1623d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,8 +1,9 @@ ext.versions = [ kotlin : '1.6.10', build_gradle : '7.1.3', - tangem_card_sdk : 'develop-159', - tangem_blockchain_sdk: 'develop-115', + tangem_card_sdk : 'develop-165', + // tangem_card_sdk : '0.0.1', + tangem_blockchain_sdk: 'develop-130', // tangem_blockchain_sdk: '0.0.1', ] diff --git a/domain/src/main/java/com/tangem/domain/DomainDialog.kt b/domain/src/main/java/com/tangem/domain/DomainDialog.kt index 3e956b77da..bc7d30a7da 100644 --- a/domain/src/main/java/com/tangem/domain/DomainDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainDialog.kt @@ -8,7 +8,7 @@ import com.tangem.network.api.tangemTech.CoinsResponse */ sealed interface DomainDialog { - data class DialogError(val error: DomainError) : DomainDialog + data class DialogError(val error: DomainModuleError) : DomainDialog data class SelectTokenDialog( val items: List, diff --git a/domain/src/main/java/com/tangem/domain/DomainError.kt b/domain/src/main/java/com/tangem/domain/DomainError.kt deleted file mode 100644 index 74ff7f01a4..0000000000 --- a/domain/src/main/java/com/tangem/domain/DomainError.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.domain - -import com.tangem.common.module.ModuleError -import com.tangem.common.module.ModuleMessage - -/** -[REDACTED_AUTHOR] - * All DomainError descendants must use their own range of codes, but no more than 999 error codes for each. - */ -sealed interface DomainModuleMessage : ModuleMessage - -sealed class DomainError( - subCode: Int, - override val message: String, - override val data: Any?, -) : DomainModuleMessage, ModuleError { - override val code: Int = ERROR_CODE_DOMAIN + subCode - - companion object { - // base code used for all error in the module - const val ERROR_CODE_DOMAIN = 10000 - const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100 -// const val CODE_ANY_OTHER = 200..299 - } -} - -sealed class AddCustomTokenError( - subCode: Int = 0 -) : DomainError(ERROR_CODE_ADD_CUSTOM_TOKEN + subCode, this::class.java.simpleName, null) { - - object FieldIsEmpty : AddCustomTokenError() - object FieldIsNotEmpty : AddCustomTokenError() - object InvalidContractAddress : AddCustomTokenError() - object NetworkIsNotSelected : AddCustomTokenError() - object InvalidDecimalsCount : AddCustomTokenError() - object InvalidDerivationPath : AddCustomTokenError() - - sealed class Network : AddCustomTokenError() { - object CheckAddressRequestError : Network() - } - - sealed class Warning : AddCustomTokenError() { - object PotentialScamToken : Warning() - object TokenAlreadyAdded : Warning() - object UnsupportedSolanaToken: Warning() - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainException.kt b/domain/src/main/java/com/tangem/domain/DomainException.kt deleted file mode 100644 index a2661db781..0000000000 --- a/domain/src/main/java/com/tangem/domain/DomainException.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.domain - -import com.tangem.common.module.FbConsumeException -import com.tangem.common.module.ModuleException - -/** -[REDACTED_AUTHOR] - */ -sealed class DomainException(override val message: String) : Throwable(message), ModuleException - -sealed class AddCustomTokenException(message: String) : DomainException(message) { - - data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException( - "Unknown network [$networkId] should not be included in the network selection dialog." - ), FbConsumeException - - data class UnAppropriateInitializationException( - val of: String, - val info: String? = null - ) : AddCustomTokenException("The [$of], must be properly initialized. Info [$info]") -} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainLayer.kt b/domain/src/main/java/com/tangem/domain/DomainLayer.kt index f83049dbf8..ef083fdc96 100644 --- a/domain/src/main/java/com/tangem/domain/DomainLayer.kt +++ b/domain/src/main/java/com/tangem/domain/DomainLayer.kt @@ -10,7 +10,7 @@ import com.tangem.domain.redux.state.ActionStateLoggerImpl object DomainLayer { internal val actionStateLogger = ActionStateLoggerImpl() - var onInitComplete: ((DomainException?) -> Unit)? = null + var onInitComplete: ((DomainModuleError?) -> Unit)? = null fun init(data: Map? = null) { initActionStateLogger() diff --git a/domain/src/main/java/com/tangem/domain/DomainModuleMessage.kt b/domain/src/main/java/com/tangem/domain/DomainModuleMessage.kt new file mode 100644 index 0000000000..6eb55484ca --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainModuleMessage.kt @@ -0,0 +1,65 @@ +package com.tangem.domain + +import com.tangem.common.module.FbConsumeException +import com.tangem.common.module.ModuleError +import com.tangem.common.module.ModuleErrorCode +import com.tangem.common.module.ModuleMessage + +/** +[REDACTED_AUTHOR] + * All DomainError descendants must use their own range of codes, but no more than 999 error codes for each. + */ +sealed interface DomainModuleMessage : ModuleMessage + +sealed class DomainModuleError( + subCode: Int, + override val message: String, + override val data: Any?, +) : DomainModuleMessage, ModuleError() { + override val code: Int = ModuleErrorCode.DOMAIN + subCode + + companion object { + // base code used for all errors in the module + internal const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100 +// const val CODE_ANY_OTHER = 200..299, 300..399 etc + } +} + +sealed class AddCustomTokenError( + subCode: Int = 0, + message: String? = null, + data: Any? = null, +) : DomainModuleError( + subCode = ERROR_CODE_ADD_CUSTOM_TOKEN + subCode, + message = message ?: this::class.java.simpleName, + data = data, +) { + + object FieldIsEmpty : AddCustomTokenError() + object FieldIsNotEmpty : AddCustomTokenError() + object InvalidContractAddress : AddCustomTokenError() + object NetworkIsNotSelected : AddCustomTokenError() + object InvalidDecimalsCount : AddCustomTokenError() + object InvalidDerivationPath : AddCustomTokenError() + + sealed class Network : AddCustomTokenError() { + object CheckAddressRequestError : Network() + } + + sealed class Warning : AddCustomTokenError() { + object PotentialScamToken : Warning() + object TokenAlreadyAdded : Warning() + object UnsupportedSolanaToken : Warning() + } + + data class SelectTokeNetworkError(val networkId: String) : AddCustomTokenError( + message = "Unknown network [$networkId] should not be included in the network selection dialog.", + ), FbConsumeException + + data class UnAppropriateInitialization( + val of: String, + val info: String? = null, + ) : AddCustomTokenError( + message = "The [$of], must be properly initialized. Info [$info]", + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt index 0325c6910d..c4f96f4b55 100644 --- a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -14,6 +14,7 @@ object NetworkLogConfig { val mercuryoService: Boolean = false val moonPayService: Boolean = false val tangemTechService: Boolean = BuildConfig.DEBUG + val paymentologyApiService: Boolean = BuildConfig.DEBUG val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG } diff --git a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt new file mode 100644 index 0000000000..c397160c2d --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt @@ -0,0 +1,92 @@ +package com.tangem.domain.common + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.CardIdRange +import com.tangem.common.contains + +object SaltPayWorkaround { + fun tokenFrom(blockchain: Blockchain): Token { + return when (blockchain) { + Blockchain.SaltPay -> Token( + name = "WXDAI", + symbol = "wxDAI", + contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417", + decimals = 18, + id = "wrapped-xdai", + ) + Blockchain.SaltPayTestnet -> Token( + name = "WXDAI Test", + symbol = "MyERC20", + contractAddress = "0x69cca8D8295de046C7c14019D9029Ccc77987A48", + decimals = 0, + id = "wrapped-xdai", + ) + else -> throw IllegalArgumentException() + } + } + + fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId) + + fun isWalletCardId(cardId: String): Boolean { + return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId) + } + + val visaBatches = listOf( + "AE02", + "AE03", + ) + attachTestVisaBatches() + + private fun attachTestVisaBatches(): List { + return listOf( + "FF03", + ) + } + + val walletCardIds = listOf( + "AC01000000033503", + "AC01000000033594", + "AC01000000033586", + "AC01000000034477", + "AC01000000032760", + "AC01000000033867", + "AC01000000032653", + "AC01000000032752", + "AC01000000034485", + "AC01000000033644", + "AC01000000037454", + "AC01000000037462", + "AC03000000076070", + "AC03000000076088", + "AC03000000076096", + "AC03000000076104", + "AC03000000076112", + "AC03000000076120", + "AC03000000076138", + "AC03000000076146", + "AC03000000076153", + "AC03000000076161", + "AC03000000076179", + "AC03000000076187", + "AC03000000076195", + "AC03000000076203", + "AC03000000076211", + "AC03000000076229", + ) + attachTestWalletCardIds() + + private fun attachTestWalletCardIds(): List { + return listOf( + "FF04000000000232", + ) + } + + val walletCardIdRanges = listOf( + CardIdRange("AC05000000000003", "AC05000000023997")!!, + ) + attachTestWalletCardIdRanges() + + private fun attachTestWalletCardIdRanges(): List { + return listOf( + CardIdRange("FF04000000000000", "FF04999999999999")!!, + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt index 35ec0dbe8b..56c6979d5e 100644 --- a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -9,6 +9,11 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.domain.common.TapWorkarounds.isSaltPay +import com.tangem.domain.common.TapWorkarounds.isSaltPayVisa +import com.tangem.domain.common.TapWorkarounds.isSaltPayWallet +import com.tangem.domain.common.TapWorkarounds.isTangemNote +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard @@ -23,17 +28,22 @@ data class ScanResponse( val walletData: WalletData?, val secondTwinPublicKey: String? = null, val derivedKeys: Map = mapOf(), - val primaryCard: PrimaryCard? = null + val primaryCard: PrimaryCard? = null, ) : CommandResponse { - fun getBlockchain(): Blockchain { - if (productType == ProductType.Note) return card.getTangemNoteBlockchain() - ?: return Blockchain.Unknown - val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown - return Blockchain.fromId(blockchainName) + return when (productType) { + ProductType.SaltPay -> if (card.isTestCard) Blockchain.SaltPayTestnet else Blockchain.SaltPay + ProductType.Note -> card.getTangemNoteBlockchain() ?: Blockchain.Unknown + else -> { + val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown + Blockchain.fromId(blockchainName) + } + } } fun getPrimaryToken(): Token? { + if (card.isSaltPay) return SaltPayWorkaround.tokenFrom(getBlockchain()) + val cardToken = walletData?.token ?: return null return Token( cardToken.name, @@ -45,14 +55,14 @@ data class ScanResponse( fun isTangemNote(): Boolean = productType == ProductType.Note fun isTangemWallet(): Boolean = productType == ProductType.Wallet + fun isSaltPay(): Boolean = productType == ProductType.SaltPay + fun isSaltPayVisa(): Boolean = card.isSaltPayVisa + fun isSaltPayWallet(): Boolean = card.isSaltPayWallet fun isTangemTwins(): Boolean = productType == ProductType.Twins - + fun twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed fun supportsBackup(): Boolean = card.settings.isBackupAllowed - fun twinsIsTwinned(): Boolean = - card.isTangemTwins() && walletData != null && secondTwinPublicKey != null - fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean { return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) } @@ -73,18 +83,22 @@ data class ScanResponse( fun hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean { val foundWallet = card.wallets.firstOrNull { it.curve == curve } ?: return false - val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false - val extendedPublicKey = extendedPublicKeysMap[derivationPath] return extendedPublicKey != null } } enum class ProductType { - Note, Twins, Wallet + Note, Twins, Wallet, SaltPay } -typealias KeyWalletPublicKey = ByteArrayKey +val Card.productType: ProductType + get() = when { + isTangemTwins -> ProductType.Twins + isTangemNote -> ProductType.Note + isSaltPay -> ProductType.SaltPay + else -> ProductType.Wallet + } -fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null \ No newline at end of file +typealias KeyWalletPublicKey = ByteArrayKey \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index a11441c51c..69ec26308b 100644 --- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -9,20 +9,26 @@ import java.util.* [REDACTED_AUTHOR] */ object TapWorkarounds { + private const val START_2_COIN_ISSUER = "start2coin" + private const val TEST_CARD_BATCH = "99FF" + private const val TEST_CARD_ID_STARTS_WITH = "FF99" + fun isStart2CoinIssuer(cardIssuer: String?): Boolean { return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER } val Card.isStart2Coin: Boolean get() = isStart2CoinIssuer(issuer.name) - val Card.isSaltPay: Boolean - get() = false //TODO fix when we know which cards are SaltPay cards + val Card.isTestCard: Boolean get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) + val Card.useOldStyleDerivation: Boolean get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" + val Card.derivationStyle: DerivationStyle? get() = if (!settings.isHDWalletAllowed) { + Int.MAX_VALUE null } else if (useOldStyleDerivation) { DerivationStyle.LEGACY @@ -30,36 +36,15 @@ object TapWorkarounds { DerivationStyle.NEW } - fun Card.isExcluded(): Boolean { - val excludedBatch = excludedBatches.contains(batchId) - val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) - return excludedBatch || excludedIssuerName - } + val Card.isNotSupportedInThatRelease: Boolean + get() = false - fun Card.isNotSupportedInThatRelease(): Boolean { - return false - } + val Card.isTangemTwins: Boolean + get() = TwinsHelper.getTwinCardNumber(cardId) != null - fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId) || isSaltPay - fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) - fun Card.getTangemNoteBlockchain(): Blockchain? = - tangemNoteBatches[batchId] ?: if (isSaltPay) Blockchain.Gnosis else null - - private const val START_2_COIN_ISSUER = "start2coin" - private const val TEST_CARD_BATCH = "99FF" - private const val TEST_CARD_ID_STARTS_WITH = "FF99" - private val excludedBatches = listOf( - "0027", - "0030", - "0031", - "0035", - ) - - private val excludedIssuers = listOf( - "TTM BANK" - ) - - private val tangemWalletBatches = listOf("AC01") + //TODO: replace by reading files from a card + val Card.isTangemNote: Boolean + get() = tangemNoteBatches.contains(batchId) private val tangemNoteBatches = mapOf( "AB01" to Blockchain.Bitcoin, @@ -76,7 +61,34 @@ object TapWorkarounds { "AB12" to Blockchain.Ethereum, ) - private val tangemWalletBatchesWithStandardDerivationType = listOf( - "AC01", "AC02", "CB95" + fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null + + + val Card.isSaltPay: Boolean + get() = isSaltPayVisa || isSaltPayWallet + + val Card.isSaltPayVisa: Boolean + get() = SaltPayWorkaround.isVisaBatchId(batchId) + + val Card.isSaltPayWallet: Boolean + get() = SaltPayWorkaround.isWalletCardId(cardId) + + + val Card.isExcluded: Boolean + get() { + val excludedBatch = excludedBatches.contains(batchId) + val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) + return excludedBatch || excludedIssuerName + } + + private val excludedBatches = listOf( + "0027", + "0030", + "0031", + "0035", + ) + + private val excludedIssuers = listOf( + "TTM BANK", ) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index fb7f671eb2..b11840f128 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { return when (networkId) { "arbitrum-one" -> Blockchain.Arbitrum - "arbitrum/test" -> Blockchain.ArbitrumTestnet + "arbitrum-one/test" -> Blockchain.ArbitrumTestnet "avalanche", "avalanche-2" -> Blockchain.Avalanche "avalanche/test", "avalanche-2/test" -> Blockchain.AvalancheTestnet "binancecoin" -> Blockchain.Binance @@ -38,12 +38,18 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "tron/test" -> Blockchain.TronTestnet "xrp", "ripple" -> Blockchain.XRP "xdai" -> Blockchain.Gnosis + "ethereum-pow-iou" -> Blockchain.EthereumPow + "ethereum-pow-iou/test" -> Blockchain.EthereumPowTestnet + "ethereumfair" -> Blockchain.EthereumFair "polkadot" -> Blockchain.Polkadot "polkadot/test" -> Blockchain.PolkadotTestnet "kusama" -> Blockchain.Kusama - "optimistic-ethereum" -> Blockchain.Optimism + // "optimistic-ethereum" -> Blockchain.Optimism + "optimistic-ethereum" -> null //TODO: Optimism is disabled until next release "optimistic-ethereum/test" -> Blockchain.OptimismTestnet "dash" -> Blockchain.Dash + "sxdai" -> Blockchain.SaltPay + "sxdai/test" -> Blockchain.SaltPayTestnet else -> null } } @@ -52,7 +58,7 @@ fun Blockchain.toNetworkId(): String { return when (this) { Blockchain.Unknown -> "unknown" Blockchain.Arbitrum -> "arbitrum-one" - Blockchain.ArbitrumTestnet -> "arbitrum/test" + Blockchain.ArbitrumTestnet -> "arbitrum-one/test" Blockchain.Avalanche -> "avalanche" Blockchain.AvalancheTestnet -> "avalanche/test" Blockchain.Binance -> "binancecoin" @@ -86,12 +92,17 @@ fun Blockchain.toNetworkId(): String { Blockchain.Tron -> "tron" Blockchain.TronTestnet -> "tron/test" Blockchain.Gnosis -> "xdai" + Blockchain.EthereumPow -> "ethereum-pow-iou" + Blockchain.EthereumPowTestnet -> "ethereum-pow-iou/test" + Blockchain.EthereumFair -> "ethereumfair" Blockchain.Polkadot -> "polkadot" Blockchain.PolkadotTestnet -> "polkadot/test" Blockchain.Kusama -> "kusama" Blockchain.Optimism -> "optimistic-ethereum" Blockchain.OptimismTestnet -> "optimistic-ethereum/test" Blockchain.Dash -> "dash" + Blockchain.SaltPay -> "sxdai" + Blockchain.SaltPayTestnet -> "sxdai/test" } } @@ -118,9 +129,21 @@ fun Blockchain.toCoinId(): String { Blockchain.XRP -> "ripple" Blockchain.Dogecoin -> "dogecoin" Blockchain.Gnosis -> "xdai" + Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> "ethereum-pow-iou" + Blockchain.EthereumFair -> "ethereumfair" Blockchain.Kusama -> "kusama" Blockchain.Optimism, Blockchain.OptimismTestnet -> "ethereum" Blockchain.Dash -> "dash" + Blockchain.SaltPay, Blockchain.SaltPayTestnet -> "xdai" Blockchain.Unknown -> "unknown" } -} \ No newline at end of file +} + +fun Blockchain.isSupportedInApp(): Boolean { + return !excludedBlockchains.contains(this) +} + +private val excludedBlockchains = listOf( + Blockchain.Optimism, // TODO: remove when fee calculation is fixed + Blockchain.SaltPay, +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index 204602df42..acc0fc7bf0 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -21,8 +21,9 @@ fun Card.supportedBlockchains(): List { (Blockchain.fromCurve(EllipticCurve.Secp256k1) + Blockchain.fromCurve(EllipticCurve.Ed25519)).distinct() } } - val filtered = supportedBlockchains.filter { isTestCard == it.isTestnet() } - return filtered + return supportedBlockchains + .filter { isTestCard == it.isTestnet() } + .filter { it.isSupportedInApp() } } fun Card.supportedTokens(): List { diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt index 0f7f23dd82..0cb52aa4a1 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt @@ -1,7 +1,11 @@ package com.tangem.domain.common.extensions +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** @@ -9,4 +13,20 @@ import kotlinx.coroutines.withContext */ suspend fun withMainContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Main, block) -suspend fun withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block) \ No newline at end of file +suspend fun withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block) + +fun debounce( + waitMs: Long = 300L, + coroutineScope: CoroutineScope, + runDestinationOn: CoroutineDispatcher = Dispatchers.Unconfined, + destinationFunction: (T) -> Unit, +): (T) -> Unit { + var debounceJob: Job? = null + return { param: T -> + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(waitMs) + withContext(runDestinationOn) { destinationFunction(param) } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt b/domain/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt new file mode 100644 index 0000000000..d3d0f8e112 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.common.extensions + +import com.tangem.common.CompletionResult +import com.tangem.common.services.Result + +/** +[REDACTED_AUTHOR] + */ +inline fun Result.successOr(failureClause: (Result.Failure) -> T): T { + return when (this) { + is Result.Success -> this.data + is Result.Failure -> failureClause(this) + } +} + +inline fun CompletionResult.successOr(failureClause: (CompletionResult.Failure) -> T): T { + return when (this) { + is CompletionResult.Success -> this.data + is CompletionResult.Failure -> failureClause(this) + } +} + diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt index 502452e476..a502ae5041 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt @@ -26,7 +26,7 @@ class AddCustomTokenService( result.data.coins.forEach { coin -> val networksWithTheSameAddress = coin.networks .filter { it.contractAddress != null || it.decimalCount != null } - .filter { it.contractAddress == contractAddress } + .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true } .filter { supportedTokenNetworkIds.contains(it.networkId) } if (networksWithTheSameAddress.isNotEmpty()) { val newToken = coin.copy(networks = networksWithTheSameAddress) @@ -45,6 +45,5 @@ class AddCustomTokenService( private fun selectNetworksForSearch(networkId: String?): String { return networkId ?: supportedTokenNetworkIds.joinToString(",") - } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index 1c09b4116a..2b56a47f90 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -5,8 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.AddCustomTokenError.Warning.* -import com.tangem.domain.AddCustomTokenException +import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken +import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded +import com.tangem.domain.AddCustomTokenError.Warning.UnsupportedSolanaToken import com.tangem.domain.DomainDialog import com.tangem.domain.DomainWrapped import com.tangem.domain.common.TapWorkarounds.derivationStyle @@ -14,10 +15,40 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.form.* -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* +import com.tangem.domain.common.form.Field +import com.tangem.domain.common.form.Form +import com.tangem.domain.common.form.TokenContractAddressValidator +import com.tangem.domain.common.form.TokenDecimalsValidator +import com.tangem.domain.common.form.TokenNameValidator +import com.tangem.domain.common.form.TokenNetworkValidator +import com.tangem.domain.common.form.TokenSymbolValidator +import com.tangem.domain.features.addCustomToken.AddCustomTokenService +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.TokenBlockchainField +import com.tangem.domain.features.addCustomToken.TokenDerivationPathField +import com.tangem.domain.features.addCustomToken.TokenField +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.FieldError +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Init +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnAddCustomTokenClicked +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnCreate +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnDestroy +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Screen +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.SetFoundTokenInfo +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.UpdateForm +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Warning +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState.Companion.createInitialScreenState import com.tangem.domain.redux.BaseStoreHub import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.ReStoreReducer @@ -168,7 +199,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT networkIdConverter = { networkId -> val blockchain = Blockchain.fromNetworkId(networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { - throw AddCustomTokenException.SelectTokeNetworkException(networkId) + throw AddCustomTokenError.SelectTokeNetworkError(networkId) } hubState.blockchainToName(blockchain) ?: "" }, @@ -456,7 +487,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT @Throws private fun throwUnAppropriateInitialization(objName: String) { - throw AddCustomTokenException.UnAppropriateInitializationException( + throw AddCustomTokenError.UnAppropriateInitialization( "AddCustomTokenHub", "$objName must be not NULL" ) } @@ -569,12 +600,12 @@ private class AddCustomTokenReducer( tangemTechService = globalState.networkServices.tangemTechService, supportedTokenNetworkIds = supportedTokenNetworkIds ) - val form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain)) state.copy( cardDerivationStyle = card.derivationStyle, form = form, tangemTechServiceManager = tangemTechServiceManager, + screenState = createInitialScreenState(card.settings.isHDWalletAllowed), ) } is OnDestroy -> { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 97c652da19..2ecd595e8e 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -9,9 +9,31 @@ import com.tangem.domain.DomainWrapped import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.supportedTokens -import com.tangem.domain.common.form.* -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* +import com.tangem.domain.common.form.CustomTokenValidator +import com.tangem.domain.common.form.DataField +import com.tangem.domain.common.form.FieldDataConverter +import com.tangem.domain.common.form.FieldId +import com.tangem.domain.common.form.FieldToJsonConverter +import com.tangem.domain.common.form.Form +import com.tangem.domain.common.form.StringIsEmptyValidator +import com.tangem.domain.common.form.StringIsNotEmptyValidator +import com.tangem.domain.common.form.TokenContractAddressValidator +import com.tangem.domain.common.form.TokenDecimalsValidator +import com.tangem.domain.common.form.TokenNameValidator +import com.tangem.domain.common.form.TokenNetworkValidator +import com.tangem.domain.common.form.TokenSymbolValidator +import com.tangem.domain.features.addCustomToken.AddCustomTokenService +import com.tangem.domain.features.addCustomToken.CustomCurrency +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.TokenBlockchainField +import com.tangem.domain.features.addCustomToken.TokenDerivationPathField +import com.tangem.domain.features.addCustomToken.TokenField import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.state.StringActionStateConverter import com.tangem.network.api.tangemTech.CoinsResponse @@ -119,7 +141,7 @@ data class AddCustomTokenState( formErrors = emptyMap(), foundToken = null, warnings = emptySet(), - screenState = createInitialScreenState(), + screenState = createInitialScreenState(card.settings.isHDWalletAllowed), tangemTechServiceManager = null, ) } @@ -223,15 +245,15 @@ data class AddCustomTokenState( return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName() } - private fun createInitialScreenState(): ScreenState { + internal fun createInitialScreenState(showDerivationPathField: Boolean = false): ScreenState { return ScreenState( contractAddressField = ViewStates.TokenField(), network = ViewStates.TokenField(), name = ViewStates.TokenField(isEnabled = false), symbol = ViewStates.TokenField(isEnabled = false), decimals = ViewStates.TokenField(isEnabled = false), - derivationPath = ViewStates.TokenField(), - addButton = ViewStates.AddButton(isEnabled = false) + derivationPath = ViewStates.TokenField(isVisible = showDerivationPathField), + addButton = ViewStates.AddButton(isEnabled = false), ) } } diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index 1f2109b371..b420816c55 100644 --- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -3,6 +3,7 @@ package com.tangem.domain.redux.global import com.tangem.domain.DomainDialog import com.tangem.domain.common.LogConfig import com.tangem.domain.common.ScanResponse +import com.tangem.network.api.paymentology.PaymentologyApiService import com.tangem.network.api.tangemTech.TangemTechService /** @@ -18,5 +19,6 @@ data class DomainGlobalState( ) data class NetworkServices( - val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService) + val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService), + val paymentologyService: PaymentologyApiService = PaymentologyApiService(LogConfig.network.paymentologyApiService), ) diff --git a/domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt b/domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt index b8291b93ac..b6e967037c 100644 --- a/domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt +++ b/domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt @@ -11,7 +11,10 @@ class BlockchainTests { fun allNetworkIdsAreImplemented() { val unimplementedIds = Blockchain.values() .toMutableList() - .apply { remove(Blockchain.Unknown) } + .apply { + remove(Blockchain.Unknown) + remove(Blockchain.Optimism) + } .map { it to Blockchain.fromNetworkId(it.toNetworkId()) } .mapNotNull { if(it.second == null) it.first else null } diff --git a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt b/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt new file mode 100644 index 0000000000..058cd368dd --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt @@ -0,0 +1,39 @@ +package com.tangem.network.api.paymentology + +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +/** +[REDACTED_AUTHOR] + */ +interface PaymentologyApi { + + @Headers("Content-Type: application/json") + @POST("card/verify") + suspend fun checkRegistration( + @Body request: CheckRegistrationRequests, + ): RegistrationResponse + + @Headers("Content-Type: application/json") + @POST("card/get_challenge") + suspend fun requestAttestationChallenge( + @Body request: CheckRegistrationRequests.Item, + ): AttestationResponse + + @Headers("Content-Type: application/json") + @POST("card/set_pin") + suspend fun registerWallet( + @Body request: RegisterWalletRequest, + ): RegisterWalletResponse + + @Headers("Content-Type: application/json") + @POST("card/kyc") + suspend fun registerKYC( + @Body request: RegisterKYCRequest, + ): RegisterWalletResponse + + companion object { + val baseUrl: String = "https://paymentologygate.oa.r.appspot.com/" + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt b/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt new file mode 100644 index 0000000000..8cd70891c3 --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt @@ -0,0 +1,63 @@ +package com.tangem.network.api.paymentology + +import com.tangem.common.extensions.toHexString +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.network.common.MoshiConverter +import com.tangem.network.common.createRetrofitInstance +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** +[REDACTED_AUTHOR] + */ +class PaymentologyApiService( + private val logEnabled: Boolean, +) { + private val api = createRetrofitInstance( + baseUrl = PaymentologyApi.baseUrl, + converterFactory = MoshiConverter.createFactory(MoshiConverter.sdkMoshi()), + logEnabled = logEnabled, + ).create(PaymentologyApi::class.java) + + suspend fun checkRegistration( + cardId: String, + publicKey: ByteArray, + ): Result = withContext(Dispatchers.IO) { + val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString()) + val request = CheckRegistrationRequests(listOf(requestItem)) + performRequest { + api.checkRegistration(request) + } + } + + suspend fun requestAttestationChallenge( + cardId: String, + publicKey: ByteArray, + ): Result = withContext(Dispatchers.IO) { + val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString()) + performRequest { + api.requestAttestationChallenge(requestItem) + } + } + + suspend fun registerWallet( + request: RegisterWalletRequest, + ): Result = withContext(Dispatchers.IO) { + performRequest { + api.registerWallet(request) + } + } + + suspend fun registerKYC( + request: RegisterKYCRequest, + ): Result = withContext(Dispatchers.IO) { + performRequest { + api.registerKYC(request) + } + } + + companion object { + fun stub(): PaymentologyApiService = PaymentologyApiService(false) + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/paymentology/Requests.kt b/network/src/main/java/com/tangem/network/api/paymentology/Requests.kt new file mode 100644 index 0000000000..580e60bfa5 --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/paymentology/Requests.kt @@ -0,0 +1,91 @@ +package com.tangem.network.api.paymentology + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.common.extensions.calculateHashCode + +/** +[REDACTED_AUTHOR] + */ +data class CheckRegistrationRequests( + val requests: List, +) { + @JsonClass(generateAdapter = true) + data class Item( + @Json(name = "CID") + val cardId: String = "", + val publicKey: String = "", + ) +} + +data class RegisterWalletRequest( + @Json(name = "CID") + val cardId: String, + val publicKey: ByteArray, + val walletPublicKey: ByteArray, + val walletSalt: ByteArray, + val walletSignature: ByteArray, + val cardSalt: ByteArray, + val cardSignature: ByteArray, + @Json(name = "PIN") + val pin: String, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RegisterWalletRequest + + if (cardId != other.cardId) return false + if (!publicKey.contentEquals(other.publicKey)) return false + if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false + if (!walletSalt.contentEquals(other.walletSalt)) return false + if (!walletSignature.contentEquals(other.walletSignature)) return false + if (!cardSalt.contentEquals(other.cardSalt)) return false + if (!cardSignature.contentEquals(other.cardSignature)) return false + if (pin != other.pin) return false + + return true + } + + override fun hashCode(): Int = calculateHashCode( + cardId.hashCode(), + publicKey.contentHashCode(), + walletPublicKey.contentHashCode(), + walletSalt.contentHashCode(), + walletSignature.contentHashCode(), + cardSalt.contentHashCode(), + cardSignature.contentHashCode(), + pin.hashCode(), + ) +} + +data class RegisterKYCRequest( + @Json(name = "CID") + val cardId: String, + val publicKey: ByteArray, + val kycProvider: String, + val kycRefId: String, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RegisterKYCRequest + + if (cardId != other.cardId) return false + if (!publicKey.contentEquals(other.publicKey)) return false + if (kycProvider != other.kycProvider) return false + if (kycRefId != other.kycRefId) return false + + return true + } + + override fun hashCode(): Int = calculateHashCode( + cardId.hashCode(), + publicKey.contentHashCode(), + kycProvider.hashCode(), + kycRefId.hashCode(), + ) +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt b/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt new file mode 100644 index 0000000000..260c9e5c9d --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt @@ -0,0 +1,81 @@ +package com.tangem.network.api.paymentology + +import com.squareup.moshi.Json +import com.tangem.common.extensions.calculateHashCode + +/** +[REDACTED_AUTHOR] + */ +data class RegistrationResponse( + val results: List = listOf(), + val success: Boolean, + val error: String?, + val errorCode: Int?, +) { + + data class Item( + @Json(name = "CID") + val cardId: String, + val passed: Boolean?, + val active: Boolean?, + @Json(name = "pin_set") + val pinSet: Boolean?, + @Json(name = "blockchain_init") + val blockchainInit: Boolean?, + @Json(name = "kyc_passed") + val kycPassed: Boolean?, + @Json(name = "kyc_provider") + val kycProvider: String?, + @Json(name = "kyc_date") + val kycDate: String?, + @Json(name = "kyc_status") + val kycStatus: KYCStatus?, + @Json(name = "disabled_by_admin") + val disabledByAdmin: Boolean?, + val error: String?, + ) +} + +enum class KYCStatus { + NOT_STARTED, + STARTED, + WAITING_FOR_APPROVAL, + CORRECTION_REQUESTED, + REJECTED, + APPROVED, +} + +data class AttestationResponse( + val challenge: ByteArray?, + val success: Boolean, + val error: String?, + val errorCode: Int?, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AttestationResponse + + if (!challenge.contentEquals(other.challenge)) return false + if (success != other.success) return false + if (error != other.error) return false + if (errorCode != other.errorCode) return false + + return true + } + + override fun hashCode(): Int = calculateHashCode( + challenge.contentHashCode(), + success.hashCode(), + error.hashCode(), + errorCode.hashCode(), + ) +} + +data class RegisterWalletResponse( + val success: Boolean, + val error: String?, + val errorCode: Int?, +) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index 4afd9ed2ef..4f88fca3b7 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -51,15 +51,15 @@ data class GeoResponse( data class UserTokensResponse( val version: Int = 0, - val group: String = "", - val sort: String = "", + val group: String? = null, + val sort: String? = null, val tokens: List = emptyList(), ) : TangemTechResponse data class TokenResponse( - val id: String, + val id: String? = null, val networkId: String, - val derivationPath: String, + val derivationPath: String? = null, val name: String, val symbol: String, val decimals: Int, diff --git a/network/src/main/java/com/tangem/network/common/NetworError.kt b/network/src/main/java/com/tangem/network/common/NetworError.kt index bdee8a8729..dfe852ecc9 100644 --- a/network/src/main/java/com/tangem/network/common/NetworError.kt +++ b/network/src/main/java/com/tangem/network/common/NetworError.kt @@ -1,6 +1,7 @@ package com.tangem.network.common import com.tangem.common.module.ModuleError +import com.tangem.common.module.ModuleErrorCode import com.tangem.common.module.ModuleMessage /** @@ -12,12 +13,11 @@ sealed class NetworkError( subCode: Int, override val message: String, override val data: Any?, -) : NetworkModuleMessage, ModuleError { - override val code: Int = ERROR_CODE_NETWORK + subCode +) : NetworkModuleMessage, ModuleError() { + override val code: Int = ModuleErrorCode.NETWORK + subCode companion object { // base code used for all error in the module - const val ERROR_CODE_NETWORK = 20000 - // const val CODE_ANY_OTHER = 100..199 + // const val CODE_ANY_OTHER = 100..199, 200..299 } } \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/NetworkException.kt b/network/src/main/java/com/tangem/network/common/NetworkException.kt deleted file mode 100644 index 033e2b9b62..0000000000 --- a/network/src/main/java/com/tangem/network/common/NetworkException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.network.common - -/** -[REDACTED_AUTHOR] - */ -interface NetworkInternalException - -sealed class NetworkException(message: String?) : Throwable(message), NetworkInternalException { -} \ No newline at end of file