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