diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..25a2f7c928 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +[*.{kt,kts}] +ktlint_ignore_back_ticked_identifier = true + +indent_size = 4 +indent_style = space +insert_final_newline = true + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true + +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.** + +max_line_length = 120 diff --git a/.gitignore b/.gitignore index 4cc00a7e1f..dd64e70b22 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ local.properties # User-specific configurations .idea +!.idea/codeStyles +!.idea/inspectionProfiles # LayoutInspector files /captures @@ -26,4 +28,4 @@ local.properties **/fastlane/report.xml **/fastlane/Preview.html **/fastlane/screenshots -**/fastlane/test_output \ No newline at end of file +**/fastlane/test_output diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..3ab743df69 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,204 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000..79ee123c2b --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000..ba5af7a4ce --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,92 @@ + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d51ad6b9a6..c37a4da509 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 31 + compileSdkVersion 32 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 31 + targetSdkVersion 32 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' @@ -83,20 +83,18 @@ dependencies { implementation implementation(project(path: ':network')) implementation implementation(project(path: ':common')) - implementation 'androidx.core:core-ktx:1.7.0' + 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.7.0" 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' - implementation 'com.tangem:blockchain:AND-1939_tron_fix_release-97' -// implementation 'com.tangem:blockchain:0.0.1' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154' + implementation "com.tangem:blockchain:${versions.tangem_blockchain_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}" // WebView implementation "androidx.browser:browser:1.3.0" @@ -145,8 +143,11 @@ dependencies { implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.okhttp3:logging-interceptor") -// -// //Crypto + // Support + implementation("com.zendesk:chat:3.3.5") + implementation("com.zendesk:messaging:5.2.4") + + //Crypto implementation "com.madgag.spongycastle:core:1.58.0.0" // animation @@ -162,14 +163,14 @@ dependencies { implementation 'com.google.android.gms:play-services-wallet:19.1.0' //Compose - implementation 'androidx.activity:activity-compose:1.4.0' + implementation 'androidx.activity:activity-compose:1.5.0' implementation 'androidx.compose.material:material:1.1.1' implementation 'androidx.compose.animation:animation:1.1.1' - implementation 'androidx.compose.foundation:foundation:1.1.1' - implementation 'androidx.compose.foundation:foundation-layout:1.1.1' + implementation 'androidx.compose.foundation:foundation:1.2.0-rc03' implementation 'androidx.compose.ui:ui:1.1.1' implementation 'androidx.compose.ui:ui-tooling:1.1.1' implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0" + implementation "com.google.accompanist:accompanist-systemuicontroller:0.23.0" implementation "com.github.skydoves:androidveil:1.1.2" diff --git a/app/libs/walletconnect-1.5.5.aar b/app/libs/walletconnect-1.5.5.aar new file mode 100644 index 0000000000..4e2f1b4fb7 Binary files /dev/null and b/app/libs/walletconnect-1.5.5.aar differ diff --git a/app/libs/walletconnect.aar b/app/libs/walletconnect.aar deleted file mode 100644 index 129de60a79..0000000000 Binary files a/app/libs/walletconnect.aar and /dev/null differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4dfca87b23..b460cf581a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,14 +28,15 @@ @@ -46,7 +47,7 @@ @@ -127,6 +128,21 @@ + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index bf673f7f88..f33f020823 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit bf673f7f88ed561cd8827447c4886f80f30b7e23 +Subproject commit f33f020823031f1c532cf5a9d5cf3fbecadcf050 diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt new file mode 100644 index 0000000000..806769df5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -0,0 +1,46 @@ +package com.tangem.tap + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import java.util.WeakHashMap +import kotlin.reflect.KClass + +class ForegroundActivityObserver { + private val activities = WeakHashMap, Activity>() + + val foregroundActivity: Activity? + get() = activities.entries + .filterNot { it.value.isDestroyed } + .firstOrNull() + ?.value + + val callbacks get() = Callbacks() + + inner class Callbacks : ActivityLifecycleCallbacks { + override fun onActivityResumed(activity: Activity) { + activities[activity::class] = activity + } + + override fun onActivityDestroyed(activity: Activity) { + activities.remove(activity::class) + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + override fun onActivityStarted(activity: Activity) { + } + override fun onActivityPaused(activity: Activity) { + } + override fun onActivityStopped(activity: Activity) { + } + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + } +} + +fun ForegroundActivityObserver.withForegroundActivity( + block: (Activity) -> Unit +) { + foregroundActivity?.let { block(it) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index a663e66e7a..4e1c24d3c9 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -5,6 +5,8 @@ import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar import com.tangem.TangemSdk @@ -57,7 +59,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) systemActions() - store.state.globalState.feedbackManager?.updateActivity(this) store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) tangemSdk = TangemSdk.init(this, TangemSdkManager.config) @@ -84,8 +85,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { } private fun systemActions() { - // makes the status bar text dark - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + WindowCompat.setDecorFitsSystemWindows(window, false) + + val windowInsetsController = WindowInsetsControllerCompat(window, binding.root) + windowInsetsController.isAppearanceLightStatusBars = true + windowInsetsController.isAppearanceLightNavigationBars = true + + supportFragmentManager.registerFragmentLifecycleCallbacks( + NavBarInsetsFragmentLifecycleCallback(), + true + ) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } diff --git a/app/src/main/java/com/tangem/tap/NavBarInsetsFragmentLifecycleCallback.kt b/app/src/main/java/com/tangem/tap/NavBarInsetsFragmentLifecycleCallback.kt new file mode 100644 index 0000000000..2239eacd70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/NavBarInsetsFragmentLifecycleCallback.kt @@ -0,0 +1,39 @@ +package com.tangem.tap + +import android.os.Bundle +import android.view.View +import androidx.compose.ui.platform.ComposeView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks + +class NavBarInsetsFragmentLifecycleCallback : FragmentLifecycleCallbacks() { + override fun onFragmentViewCreated( + fm: FragmentManager, + f: Fragment, + v: View, + savedInstanceState: Bundle?, + ) { + if (v is ComposeView) return + + ViewCompat.setOnApplyWindowInsetsListener(v) { view, windowInsets -> + val statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()) + val navigationBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + if (view.fitsSystemWindows) { + view.updatePadding( + top = statusBarInsets.top, + bottom = navigationBarInsets.bottom, + ) + } else { + view.updatePadding( + bottom = navigationBarInsets.bottom, + ) + } + windowInsets + } + } +} diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 2e6c7b534b..0c69e5de88 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -8,11 +8,15 @@ import com.google.firebase.ktx.Firebase import com.google.firebase.remoteconfig.ktx.remoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.tangem.Log +import com.tangem.LogFormat import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.domain.DomainLayer import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.analytics.GlobalAnalyticsHandler +import com.tangem.tap.common.feedback.AdditionalFeedbackInfo +import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.images.createCoilImageLoader +import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction @@ -24,9 +28,6 @@ import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.domain.walletconnect.WalletConnectRepository -import com.tangem.tap.features.feedback.AdditionalEmailInfo -import com.tangem.tap.features.feedback.FeedbackManager -import com.tangem.tap.features.feedback.TangemLogCollector import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig @@ -40,6 +41,8 @@ val store = Store( ) val logConfig = LogConfig() +lateinit var foregroundActivityObserver: ForegroundActivityObserver + lateinit var preferencesStorage: PreferencesStorage lateinit var currenciesRepository: CurrenciesRepository lateinit var walletConnectRepository: WalletConnectRepository @@ -68,6 +71,8 @@ class TapApplication : Application(), ImageLoaderFactory { ) walletConnectRepository = WalletConnectRepository(this) + foregroundActivityObserver = ForegroundActivityObserver() + registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) initFeedbackManager() loadConfigs() @@ -85,22 +90,51 @@ class TapApplication : Application(), ImageLoaderFactory { val localLoader = FeaturesLocalLoader(this, moshi) val remoteLoader = FeaturesRemoteLoader(moshi) val configManager = ConfigManager(localLoader, remoteLoader) - configManager.load { + configManager.load { config -> store.dispatch(GlobalAction.SetConfigManager(configManager)) - shopService = TangemShopService(this, configManager.config.shopify!!) + shopService = TangemShopService( + application = this, + shopifyShop = config.shopify!! + ) + store.state.globalState.feedbackManager?.initChat( + context = this, + zendeskConfig = config.zendesk!! + ) } val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi)) warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) } } private fun initFeedbackManager() { - val infoHolder = AdditionalEmailInfo() + val infoHolder = AdditionalFeedbackInfo() infoHolder.setAppVersion(this) - val logWriter = TangemLogCollector() + val logLevels = listOf( + Log.Level.ApduCommand, + Log.Level.Apdu, + Log.Level.Tlv, + Log.Level.Nfc, + Log.Level.Command, + Log.Level.Session, + Log.Level.View, + Log.Level.Network, + Log.Level.Error, + ) + val logWriter = TangemLogCollector( + levels = logLevels, + messageFormatter = LogFormat.StairsFormatter(), + ) Log.addLogger(logWriter) - store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter))) + store.dispatch( + GlobalAction.SetFeedbackManager( + FeedbackManager( + infoHolder = infoHolder, + logCollector = logWriter, + preferencesStorage = preferencesStorage, + ), + ), + ) } private fun initAppsFlyer() { @@ -115,4 +149,5 @@ class TapApplication : Application(), ImageLoaderFactory { data class LogConfig( val coil: Boolean = BuildConfig.DEBUG, val storeAction: Boolean = BuildConfig.DEBUG, + val zendesk: Boolean = BuildConfig.DEBUG, ) \ 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 9af6c88b29..96cf1b874b 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -4,12 +4,14 @@ import android.app.Dialog import android.content.Context import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.common.ui.SimpleAlertDialog +import com.tangem.tap.common.ui.SimpleCancelableAlertDialog import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog +import com.tangem.tap.features.details.ui.walletconnect.dialogs.ChooseNetworkDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog -import com.tangem.tap.features.details.ui.walletconnect.dialogs.SimpleAlertDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -19,7 +21,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBack import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog -import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionBottomSheetDialog import com.tangem.tap.features.wallet.ui.dialogs.RussianCardholdersWarningBottomSheetDialog @@ -73,22 +75,33 @@ class DialogManager : StoreSubscriber { messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet, 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 + ), + context = context + ) is WalletConnectDialog.OpeningSessionRejected -> { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_same_wcuri, - context = context + context = context, ) } is WalletConnectDialog.SessionTimeout -> { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_error_timeout, - context = context + context = context, ) } is WalletConnectDialog.ApproveWcSession -> - ApproveWcSessionDialog.create(state.dialog.session, context) + ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context) + is WalletConnectDialog.ChooseNetwork -> + ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context) is WalletConnectDialog.ClipboardOrScanQr -> ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) is WalletConnectDialog.RequestTransaction -> @@ -102,6 +115,12 @@ class DialogManager : StoreSubscriber { sessionId = state.dialog.sessionId, cardId = state.dialog.cardId, dAppName = state.dialog.dAppName, + context = context, + ) + is WalletConnectDialog.UnsupportedNetwork -> + SimpleAlertDialog.create( + titleRes = R.string.wallet_connect, + messageRes = R.string.wallet_connect_scanner_error_unsupported_network, context = context ) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) @@ -116,6 +135,20 @@ class DialogManager : StoreSubscriber { 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 + ), + context = context, + ) + is WalletDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create( + title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle), + messageRes = state.dialog.messageRes, + context = context, + primaryButtonRes = state.dialog.primaryButtonRes, + primaryButtonAction = state.dialog.onOk + ) is WalletDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(context) else -> null diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt b/app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt similarity index 93% rename from app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt rename to app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt index 2bb6c5b8d5..324fd3877e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt @@ -1,6 +1,5 @@ -package com.tangem.tap.features.home.compose +package com.tangem.tap.common.compose -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -12,7 +11,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp -@OptIn(ExperimentalAnimationApi::class) @Composable fun TextAutoSize( modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt index b31a15c4ae..ce0b190598 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -38,6 +38,11 @@ fun SpacerH24(modifier: Modifier = Modifier) { SpacerH(24.dp, modifier) } +@Composable +fun SpacerH32(modifier: Modifier = Modifier) { + SpacerH(32.dp, modifier) +} + // ***************************** Vertical @Composable fun SpacerW(width: Dp, modifier: Modifier = Modifier) { @@ -64,6 +69,11 @@ fun SpacerW24(modifier: Modifier = Modifier) { SpacerW(24.dp, modifier) } +@Composable +fun SpacerW32(modifier: Modifier = Modifier) { + SpacerW(32.dp, modifier) +} + // ***************************** Size @Composable fun SpacerS(size: Dp, modifier: Modifier = Modifier) { @@ -88,4 +98,9 @@ fun SpacerS16(modifier: Modifier = Modifier) { @Composable fun SpacerS24(modifier: Modifier = Modifier) { SpacerS(24.dp, modifier) +} + +@Composable +fun SpacerS32(modifier: Modifier = Modifier) { + SpacerS(32.dp, modifier) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt new file mode 100644 index 0000000000..88b088fa6a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.common.compose + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.sp +import com.tangem.wallet.R + +object TangemTypography { + + val body1 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto)), + fontSize = 16.0.sp, + letterSpacing = 0.5.sp, + lineHeight = 24.0.sp, + ) + val body2 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto)), + fontSize = 14.0.sp, + letterSpacing = 0.25.sp, + lineHeight = 20.0.sp, + ) + val button = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 14.0.sp, + letterSpacing = 0.10000000149011612.sp, + lineHeight = 20.0.sp, + ) + val caption = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto)), + fontSize = 12.0.sp, + letterSpacing = 0.4000000059604645.sp, + lineHeight = 16.0.sp, + ) + val headline1 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto)), + fontSize = 34.0.sp, + letterSpacing = 0.0.sp, + lineHeight = 44.0.sp, + ) + val headline2 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 24.0.sp, + letterSpacing = 0.18000000715255737.sp, + lineHeight = 32.0.sp, + ) + val headline3 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 20.0.sp, + letterSpacing = 0.15000000596046448.sp, + lineHeight = 24.0.sp, + ) + val overline = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 10.0.sp, + letterSpacing = 1.5.sp, + lineHeight = 16.0.sp, + ) + val subtitle1 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 16.0.sp, + letterSpacing = 0.15000000596046448.sp, + lineHeight = 24.0.sp, + ) + val subtitle2 = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_medium)), + fontSize = 14.0.sp, + letterSpacing = 0.10000000149011612.sp, + lineHeight = 24.0.sp, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt new file mode 100644 index 0000000000..1952c6a058 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.animation.core.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember + +/** +[REDACTED_AUTHOR] + */ +typealias AnimatedValue = Pair + +@Composable +fun AnimatedValue.toAnimatable( + isPaused: Boolean, + duration: Int, + easing: Easing = LinearEasing, +): Animatable { + return animatable( + values = this, + isPaused = isPaused, + duration = duration, + easing = easing, + ) +} + +@Composable +fun animatable( + values: AnimatedValue, + duration: Int, + isPaused: Boolean = false, + easing: Easing = LinearEasing, +): Animatable { + val animatable = remember { Animatable(values.first) } + + LaunchedEffect(isPaused) { + if (isPaused) { + animatable.stop() + } else { + animatable.animateTo( + targetValue = values.second, + animationSpec = tween( + durationMillis = duration, + easing = easing + ) + ) + } + } + return animatable +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt new file mode 100644 index 0000000000..42acdcf306 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun Dp.toPx(): Float { + val currentDp = this + return with(LocalDensity.current) { currentDp.toPx() } +} + +fun DpSize.halfWidth(): Dp = this.width / 2 + +fun DpSize.halfHeight(): Dp = this.height / 2 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt new file mode 100644 index 0000000000..1692b4c51f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.annotation.DrawableRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.core.graphics.drawable.toBitmap + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap { + val drawable = AppCompatResources.getDrawable(LocalContext.current, drawableId) + ?: throw NullPointerException() + return drawable.toBitmap().asImageBitmap() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt new file mode 100644 index 0000000000..459086b9c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.MutableState + +/** +[REDACTED_AUTHOR] + */ +fun MutableState>.addAndNotify(value: T) { + this.value = this.value.toMutableList().apply { add(value) } +} + +fun MutableState>.removeAndNotify(value: T) { + this.value = this.value.toMutableList().apply { remove(value) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt new file mode 100644 index 0000000000..fa91934a30 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.tangem_sdk_new.extensions.pxToDp + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun Painter.dpSize(): DpSize = DpSize( + intrinsicSize.width.pxToDp().dp, + intrinsicSize.height.pxToDp().dp, +) + +@Composable +private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this) + +@Composable +private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt new file mode 100644 index 0000000000..f2897e2b63 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.common.extensions + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchain.common.BlockchainError +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.card.Card +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.AnalyticsHandler +import com.tangem.tap.common.analytics.AnalyticsParam +import com.tangem.tap.features.demo.DemoTransactionSender + +/** +[REDACTED_AUTHOR] + */ +fun AnalyticsHandler.logSendTransactionError( + error: BlockchainError, + action: Analytics.ActionToLog, + parameters: Map? = mapOf(), + card: Card? = null, +) { + when (val blockchainSdkError = (error as BlockchainSdkError)) { + is BlockchainSdkError.WrappedTangemError -> { + val tangemSdkError = (blockchainSdkError.tangemError as? TangemSdkError) ?: return + + logCardSdkError( + error = tangemSdkError, + actionToLog = action, + parameters = parameters, + card = card, + ) + } + else -> { + when { + blockchainSdkError.customMessage.contains(DemoTransactionSender.ID) -> return + else -> { + val params = parameters?.toMutableMap() ?: mutableMapOf() + params[AnalyticsParam.ACTION] = action.key + params[AnalyticsParam.ERROR_CODE] = error.code.toString() + params[AnalyticsParam.ERROR_DESCRIPTION] = "${error.javaClass.simpleName}: ${error.customMessage}" + params[AnalyticsParam.ERROR_KEY] = "BlockchainSdkError" + + FirebaseCrashlytics.getInstance().apply { + params.forEach { setCustomKey(it.key.param, it.value) } + recordException(error) + } + } + } + } + } +} \ No newline at end of file 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 3cb5aa96c8..e2490e7886 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 @@ -27,6 +27,7 @@ fun Blockchain.getRoundIconRes(): Int { Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_round + Blockchain.Gnosis -> R.drawable.ic_gnosis_round else -> R.drawable.ic_tangem_logo } } @@ -53,6 +54,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color 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 else -> R.drawable.ic_tangem_logo } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 89d9327ed6..a9504e668a 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -7,11 +7,13 @@ import androidx.fragment.app.FragmentManager import com.tangem.common.extensions.VoidCallback import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.FragmentShareTransition -import com.tangem.tap.features.details.ui.DetailsConfirmFragment -import com.tangem.tap.features.details.ui.DetailsFragment -import com.tangem.tap.features.details.ui.DetailsSecurityFragment +import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment +import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment +import com.tangem.tap.features.details.ui.details.DetailsFragment +import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment +import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment -import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment +import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment @@ -61,7 +63,11 @@ fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) { } fun FragmentActivity.getPreviousScreen(): AppScreen? { - val indexOfLastFragment = this.supportFragmentManager.backStackEntryCount - 1 + val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) { + this.supportFragmentManager.backStackEntryCount - 1 + } else { + 0 + } val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount) this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name else null @@ -88,13 +94,15 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.Wallet -> WalletFragment() AppScreen.Send -> SendFragment() AppScreen.Details -> DetailsFragment() - AppScreen.DetailsConfirm -> DetailsConfirmFragment() - AppScreen.DetailsSecurity -> DetailsSecurityFragment() + AppScreen.DetailsSecurity -> SecurityModeFragment() + AppScreen.CardSettings -> CardSettingsFragment() + AppScreen.AppSettings -> AppSettingsFragment() + AppScreen.ResetToFactory -> ResetCardFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> AddTokensFragment() AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> WalletDetailsFragment() - AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment() + AppScreen.WalletConnectSessions -> WalletConnectFragment() AppScreen.QrScan -> QrScanFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 219e9339da..795bc8e268 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -116,6 +116,8 @@ fun BigDecimal.formatAmountAsSpannedString( val integer = amount.substringBefore('.') val reminder = amount.substringAfter('.') + // test formatter log Log.e("TEST ", BigDecimal("1234567890987654321.1234567890987654321").formatWithSpaces()) + return buildSpannedString { append(integer) append('.') @@ -136,7 +138,7 @@ fun BigDecimal.formatWithSpaces(): String { var index: Int = integerStr.length while (0 < index) { if (index <= 3) { - packets.add(0, integerStr) + packets.add(integerStr) break } index -= 3 @@ -145,7 +147,7 @@ fun BigDecimal.formatWithSpaces(): String { } return buildString { - packets.forEachIndexed { index, packet -> + packets.reversed().forEachIndexed { index, packet -> append(packet) if (index != packets.lastIndex) append(' ') } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt b/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt index 3f8fa992ff..961e77e8f7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt @@ -19,4 +19,6 @@ fun StringBuilder.appendIfNotNull(value: String?, prefix: String? = null, postfi fun String.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): String { return StringBuilder(this).apply { appendIfNotNull(value, prefix, postfix) }.toString() -} \ No newline at end of file +} + +fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count)) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 8c2184ae50..617f69c36d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -93,32 +93,39 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U } fun Context.dpToPixels(dp: Int): Int = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics - ).toInt() + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics + ).toInt() fun Context.pixelsToDp(pixels: Int): Int { return (pixels.toFloat() / - (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)) + (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)) .toInt() } +fun Context.dpToPixels(dp: Float): Float = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics) + + +fun Context.pixelsToDp(pixels: Float): Float = + (pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)) + tailrec fun Context?.getActivity(): Activity? = this as? Activity - ?: (this as? ContextWrapper)?.baseContext?.getActivity() + ?: (this as? ContextWrapper)?.baseContext?.getActivity() fun MaterialCardView.setMargins( - marginLeftDp: Int = 16, - marginTopDp: Int = 8, - marginRightDp: Int = 16, - marginBottomDp: Int = 8 + marginLeftDp: Int = 16, + marginTopDp: Int = 8, + marginRightDp: Int = 16, + marginBottomDp: Int = 8 ) { val params = this.layoutParams (params as ViewGroup.MarginLayoutParams).setMargins( - context.dpToPixels(marginLeftDp), - context.dpToPixels(marginTopDp), - context.dpToPixels(marginRightDp), - context.dpToPixels(marginBottomDp) + context.dpToPixels(marginLeftDp), + context.dpToPixels(marginTopDp), + context.dpToPixels(marginRightDp), + context.dpToPixels(marginBottomDp) ) this.layoutParams = params } @@ -128,11 +135,11 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) { val flags = this.window.decorView.systemUiVisibility // Update the SystemUiVisibility dependening on whether we want a Light or Dark theme. this.window.decorView.systemUiVisibility = - if (setTextDark) { - flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - } else { - flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } + if (setTextDark) { + flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } else { + flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } } } @@ -150,7 +157,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") { fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - ?: return default + ?: return default val clipData = clipboard.primaryClip ?: return default if (clipData.itemCount == 0) return default @@ -172,10 +179,10 @@ fun Fragment.shareText(text: String) { } fun Context.safeStartActivity( - intent: Intent, - options: Bundle? = null, - fallback: ((ActivityNotFoundException) -> Unit)? = null, - finally: VoidCallback? = null + intent: Intent, + options: Bundle? = null, + fallback: ((ActivityNotFoundException) -> Unit)? = null, + finally: VoidCallback? = null ) { try { this.startActivity(intent, options) @@ -187,7 +194,7 @@ fun Context.safeStartActivity( } fun View.getString(resId: Int, vararg formatArgs: Any?): String { - return context.getString(resId, formatArgs) + return context.getString(resId, *formatArgs) } fun View.animateVisibility( 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 40acdd1b7d..311932f7c1 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 @@ -8,7 +8,7 @@ import com.tangem.tap.common.TestActions import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken -import com.tangem.tap.features.demo.isDemoWallet +import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.reducers.createAddressesData import com.tangem.tap.network.NetworkConnectivity @@ -21,7 +21,9 @@ import timber.log.Timber [REDACTED_AUTHOR] */ suspend fun WalletManager.safeUpdate(): Result = try { - if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) { + val scanResponse = store.state.globalState.scanResponse + + if (scanResponse?.isDemoCard() == true || TestActions.testAmountInjectionForWalletManagerEnabled) { delay(500) TestActions.testAmountInjectionForWalletManagerEnabled = false Result.Success(wallet) @@ -49,11 +51,10 @@ suspend fun WalletManager.safeUpdate(): Result = try { fun WalletManager?.getToUpUrl(): String? { val globalState = store.state.globalState - val exchangeManager = globalState.exchangeManager ?: return null val wallet = this?.wallet ?: return null val defaultAddress = wallet.address - return exchangeManager.getUrl( + return globalState.exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Buy, blockchain = wallet.blockchain, cryptoCurrencyName = wallet.blockchain.currency, diff --git a/app/src/main/java/com/tangem/tap/common/feature/Feature.kt b/app/src/main/java/com/tangem/tap/common/feature/Feature.kt new file mode 100644 index 0000000000..ce1313c772 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feature/Feature.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.feature + +/** +[REDACTED_AUTHOR] + */ +interface Feature { + fun featureIsSwitchedOn():Boolean +} \ 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 new file mode 100644 index 0000000000..ddc7a41b23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -0,0 +1,127 @@ +package com.tangem.tap.common.feedback + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +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.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 appVersion: String = "" + + // card + var cardId: String = "" + var cardFirmwareVersion: String = "" + var cardIssuer: String = "" + var cardBlockchain: String = "" + + // wallets + internal val walletsInfo = mutableListOf() + internal val tokens = mutableMapOf>() + internal var onSendErrorWalletInfo: EmailWalletInfo? = null + var signedHashesCount: String = "" + + // device + var phoneModel: String = Build.MODEL + var osVersion: String = Build.VERSION.SDK_INT.toString() + + // send error + var destinationAddress: String = "" + var amount: String = "" + var fee: String = "" + var token: String = "" + + fun setAppVersion(context: Context) { + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + appVersion = pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + } + + fun setCardInfo(data: ScanResponse) { + cardId = data.card.cardId + cardBlockchain = data.walletData?.blockchain ?: "" + cardFirmwareVersion = data.card.firmwareVersion.stringValue + cardIssuer = data.card.issuer.name + signedHashesCount = data.card.wallets + .joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" } + } + + 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 ?: "" + ) + ) + if (manager.cardTokens.isNotEmpty()) { + tokens[manager.wallet.blockchain] = manager.cardTokens + } + } + } + + fun updateOnSendError( + wallet: Wallet, + host: String, + amountToSend: Amount, + feeAmount: Amount, + destinationAddress: String, + ) { + onSendErrorWalletInfo = EmailWalletInfo( + blockchain = wallet.blockchain, + address = getAddress(wallet), + explorerLink = getExploreUri(wallet), + host = host, + derivationPath = wallet.publicKey.derivationPath?.rawPath ?: "" + ) + + 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 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" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt new file mode 100644 index 0000000000..6578b97356 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -0,0 +1,109 @@ +package com.tangem.tap.common.feedback + +import android.content.Context +import com.tangem.domain.common.TapWorkarounds +import com.tangem.tap.common.extensions.breakLine +import com.tangem.wallet.R + +interface FeedbackData { + val subjectResId: Int + val mainMessageResId: Int + + fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message + + fun prepare(infoHolder: AdditionalFeedbackInfo) {} + + fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String + + fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String { + return StringBuilder().apply { + append(context.getString(mainMessageResId)) + breakLine(3) + append(context.getString(getDataCollectionMessageResId())) + breakLine() + append(createOptionalMessage(infoHolder)) + }.toString() + } +} + +class RateCanBeBetterEmail : FeedbackData { + override val subjectResId: Int = R.string.feedback_subject_rate_negative + override val mainMessageResId: Int = R.string.feedback_preface_rate_negative + + override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder) + .appendCardInfo() + .breakLine() + .appendPhoneInfo() + .build() +} + +class ScanFailsEmail : FeedbackData { + + override val subjectResId: Int = R.string.feedback_subject_scan_failed + override val mainMessageResId: Int = R.string.feedback_preface_scan_failed + + override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply { + append(context.getString(mainMessageResId)) + breakLine(4) + append(createOptionalMessage(infoHolder)) + }.toString() + + override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder) + .appendPhoneInfo() + .build() +} + +class SendTransactionFailedEmail( + val error: String, +) : FeedbackData { + + override val subjectResId: Int = R.string.feedback_subject_tx_failed + override val mainMessageResId: Int = R.string.feedback_preface_tx_failed + + override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder) + .appendCardInfo() + .appendDelimiter() + .appendTxFailedBlockchainInfo(error) + .breakLine() + .appendPhoneInfo() + .build() +} + +class FeedbackEmail : FeedbackData { + override val subjectResId: Int + get() = if (isS2CCard) s2cSubject else tangemSubject + override val mainMessageResId: Int + get() = if (isS2CCard) s2cMainMessage else tangemMainMessage + + private val tangemSubject: Int = R.string.feedback_subject_support_tangem + private val tangemMainMessage: Int = R.string.feedback_preface_support + + private val s2cSubject: Int = R.string.feedback_subject_support + private val s2cMainMessage: Int = R.string.feedback_preface_support + + private var isS2CCard = false + + override fun prepare(infoHolder: AdditionalFeedbackInfo) { + isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer) + } + + override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder) + .appendCardInfo() + .appendWalletsInfo() + .breakLine() + .appendPhoneInfo() + .build() +} + +class SupportInfo : FeedbackData { + override val subjectResId: Int = R.string.details_ask_a_question + override val mainMessageResId: Int = R.string.details_ask_a_question + + override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String { + return FeedbackDataBuilder(infoHolder) + .appendCardInfo() + .appendDelimiter() + .appendPhoneInfo() + .build() + } +} \ 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 new file mode 100644 index 0000000000..a3a9ee5575 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.common.feedback + +import com.tangem.tap.common.extensions.breakLine + +class FeedbackDataBuilder( + private val infoHolder: AdditionalFeedbackInfo +) { + val builder = StringBuilder() + + fun appendDelimiter(): FeedbackDataBuilder { + builder.appendDelimiter() + return this + } + + fun breakLine(count: Int = 1): FeedbackDataBuilder { + builder.breakLine(count) + return this + } + + fun appendCardInfo(): 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) + return this + } + + fun appendWalletsInfo(): 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) + + 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("Contract address", token.contractAddress) + } + } + } + return this + } + + fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder { + val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo() + builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName) + builder.appendKeyValue("Derivation path", walletInfo.derivationPath) + builder.appendKeyValue("Host", walletInfo.host) + builder.appendKeyValue("Token", infoHolder.token) + builder.appendKeyValue("Error", error) + builder.appendDelimiter() + builder.appendKeyValue("Source address", walletInfo.address) + builder.appendKeyValue("Destination address", infoHolder.destinationAddress) + builder.appendKeyValue("Amount", infoHolder.amount) + builder.appendKeyValue("Fee", infoHolder.fee) + return this + } + + fun appendPhoneInfo(): FeedbackDataBuilder { + builder.appendKeyValue("Phone model", infoHolder.phoneModel) + builder.appendKeyValue("OS version", infoHolder.osVersion) + builder.appendKeyValue("App version", infoHolder.appVersion) + return this + } + + 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.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 new file mode 100644 index 0000000000..ddd4e6ff6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -0,0 +1,143 @@ +package com.tangem.tap.common.feedback + +import android.content.Context +import android.os.Build +import com.tangem.domain.common.TapWorkarounds +import com.tangem.tap.common.extensions.sendEmail +import com.tangem.tap.common.log.TangemLogCollector +import com.tangem.tap.common.zendesk.ZendeskConfig +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.logConfig +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 +import zendesk.chat.ChatEngine +import zendesk.chat.ChatProvidersConfiguration +import zendesk.chat.VisitorInfo +import zendesk.configurations.Configuration +import zendesk.messaging.MessagingActivity +import java.io.File +import java.io.FileWriter +import java.io.StringWriter + +/** +[REDACTED_AUTHOR] + */ +class FeedbackManager( + val infoHolder: AdditionalFeedbackInfo, + private val logCollector: TangemLogCollector, + private val preferencesStorage: PreferencesStorage, +) { + fun initChat( + context: Context, + zendeskConfig: ZendeskConfig, + ) { + Chat.INSTANCE.init( + context, + zendeskConfig.accountKey, + zendeskConfig.appId + ) + + // Zendesk logs + Logger.setLoggable(logConfig.zendesk) + } + + fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { + feedbackData.prepare(infoHolder) + foregroundActivityObserver.withForegroundActivity { activity -> + val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null + activity.sendEmail( + email = getSupportEmail(), + subject = activity.getString(feedbackData.subjectResId), + message = feedbackData.joinTogether(activity, infoHolder), + file = fileLog, + onFail = onFail + ) + } + } + + fun openChat(feedbackData: FeedbackData) { + feedbackData.prepare(infoHolder) + foregroundActivityObserver.withForegroundActivity { activity -> + setChatVisitorInfo() + setChatVisitorNote(activity, feedbackData) + showMessagingActivity(activity) + } + } + + private fun createLogFile(context: Context): File? { + return try { + val file = File(context.filesDir, "logs.txt") + file.delete() + file.createNewFile() + + val stringWriter = StringWriter() + logCollector.getLogs().forEach { stringWriter.append(it) } + val fileWriter = FileWriter(file) + fileWriter.write(stringWriter.toString()) + fileWriter.close() + logCollector.clearLogs() + file + } catch (ex: Exception) { + Timber.e(ex, "Can't create the logs file") + null + } + } + + private fun setChatVisitorInfo() { + if (preferencesStorage.chatFirstLaunchTime == null) { + preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis() + } + val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode() + val visitorInfo = VisitorInfo.builder() + .withName("User $chatUserId") + .build() + + Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder() + .withVisitorInfo(visitorInfo) + .build() + } + + private fun setChatVisitorNote( + context: Context, + feedbackData: FeedbackData, + ) { + Chat.INSTANCE.providers() + ?.profileProvider() + ?.setVisitorNote(feedbackData.joinTogether(context, infoHolder)) + } + + private fun showMessagingActivity(context: Context) { + MessagingActivity.builder() + .withMultilineResponseOptionsEnabled(false) + .withBotLabelStringRes(R.string.chat_bot_name) + .withBotAvatarDrawable(R.mipmap.ic_launcher) + .withEngines(ChatEngine.engine()) + .show(context, buildChatConfig()) + } + + private fun buildChatConfig(): Configuration { + return ChatConfiguration.builder() + .withOfflineFormEnabled(true) + .withAgentAvailabilityEnabled(true) + .withPreChatFormEnabled(false) + .build() + } + + private fun getSupportEmail(): String { + return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) { + S2C_SUPPORT_EMAIL + } else { + DEFAULT_SUPPORT_EMAIL + } + } + + companion object { + const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" + const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt new file mode 100644 index 0000000000..54a56e17c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.common.log + +import com.tangem.Log +import com.tangem.LogFormat +import com.tangem.TangemSdkLogger +import java.text.SimpleDateFormat +import java.util.* + +class TangemLogCollector( + private val levels: List, + private val messageFormatter: LogFormat, +) : TangemSdkLogger { + + private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS") + private val logs = mutableListOf() + private val mutex = Object() + + override fun log(message: () -> String, level: Log.Level) { + if (!levels.contains(level)) return + + synchronized(mutex) { + val formattedMessage = messageFormatter.format(message, level) + val logMessage = "${dateFormatter.format(Date())}: $formattedMessage" + logs.add("$logMessage\n") + } + } + + fun getLogs(): List = synchronized(mutex) { logs.toList() } + + fun clearLogs() = synchronized(mutex) { logs.clear() } +} \ No newline at end of file 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 591a20e896..89d609b32c 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 @@ -2,8 +2,8 @@ package com.tangem.tap.common.redux import com.tangem.common.extensions.VoidCallback import com.tangem.tap.common.TestAction +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.AddressData -import com.tangem.tap.features.wallet.redux.Currency /** [REDACTED_AUTHOR] 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 a82d5ba94b..4927405ccd 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 @@ -7,14 +7,18 @@ import com.tangem.common.core.TangemError import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.GlobalAnalyticsHandler import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.redux.* +import com.tangem.tap.common.feedback.FeedbackData +import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.redux.DebugErrorAction +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.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.details.redux.SecurityOption -import com.tangem.tap.features.feedback.EmailData -import com.tangem.tap.features.feedback.FeedbackManager import org.rekotlin.Action sealed class GlobalAction : Action { @@ -73,7 +77,8 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction() - data class SendFeedback(val emailData: EmailData) : GlobalAction() + data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() + data class OpenChat(val feedbackData: FeedbackData) : 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 344a11cc29..4d227fcd92 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 @@ -15,6 +15,7 @@ import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService @@ -23,11 +24,11 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import java.util.Locale import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware +import java.util.* class GlobalMiddleware { companion object { @@ -62,9 +63,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } is GlobalAction.RestoreAppCurrency -> { - store.dispatch(GlobalAction.RestoreAppCurrency.Success( - preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency() - )) + store.dispatch( + GlobalAction.RestoreAppCurrency.Success( + preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency(), + ), + ) } is GlobalAction.HideWarningMessage -> { store.state.globalState.warningManager?.let { @@ -79,8 +82,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } } - is GlobalAction.SendFeedback -> { - store.state.globalState.feedbackManager?.send(action.emailData) + is GlobalAction.SendEmail -> { + store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) + } + is GlobalAction.OpenChat -> { + store.state.globalState.feedbackManager?.openChat(action.feedbackData) } is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) @@ -104,7 +110,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di secret = mercuryoSecret, ) val sellService = MoonPayService(moonPayKey, moonPaySecretKey) - val exchangeManager = CurrencyExchangeManager(buyService, sellService) + val cardProvider = { store.state.globalState.scanResponse?.card } + + val exchangeManager = CurrencyExchangeManager( + buyService = buyService, + sellService = sellService, + primaryRules = CardExchangeRules(cardProvider), + ) store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } @@ -124,7 +136,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di store.state.globalState.analyticsHandlers, currenciesRepository, action.additionalBlockchainsToDerive, - action.messageResId + action.messageResId, ) withMainContext { store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) @@ -147,15 +159,15 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is Result.Success -> { store.dispatchOnMain( GlobalAction.FetchUserCountry.Success( - countryCode = result.data.code.lowercase() - ) + countryCode = result.data.code.lowercase(), + ), ) } is Result.Failure -> { store.dispatchOnMain( GlobalAction.FetchUserCountry.Success( - countryCode = Locale.getDefault().country.lowercase() - ) + countryCode = Locale.getDefault().country.lowercase(), + ), ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 8588256411..1fed436b47 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.AnalyticsHandler import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.domain.PayIdManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import org.rekotlin.StateType @@ -25,7 +25,7 @@ data class GlobalState( val appCurrency: FiatCurrency = FiatCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, - val exchangeManager: CurrencyExchangeManager? = null, + val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val resources: AndroidResources = AndroidResources(), val analyticsHandlers: AnalyticsHandler? = null, val userCountryCode: String? = null, diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt index 214a034270..0579c7f651 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux.navigation +import android.net.Uri import androidx.fragment.app.FragmentActivity import org.rekotlin.Action import java.lang.ref.WeakReference @@ -15,6 +16,8 @@ sealed class NavigationAction : Action { data class OpenUrl(val url: String) : NavigationAction() + data class OpenDocument(val url: Uri) : NavigationAction() + data class Share(val data: String) : NavigationAction() data class ActivityCreated(val activity: WeakReference) : NavigationAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index e8e0168455..8a2f5c5ac1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux.navigation +import android.content.Intent import com.tangem.tap.common.CustomTabsManager import com.tangem.tap.common.extensions.openFragment import com.tangem.tap.common.extensions.popBackTo @@ -32,6 +33,11 @@ val navigationMiddleware: Middleware = { dispatch, state -> CustomTabsManager().openUrl(action.url, it) } } + is NavigationAction.OpenDocument -> { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = action.url + navState?.activity?.get()?.startActivity(intent) + } is NavigationAction.Share -> { navState?.activity?.get()?.let { it.shareText(action.data) diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 1fc6832148..9c6468c049 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -16,7 +16,7 @@ enum class AppScreen { OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther, Wallet, WalletDetails, Send, - Details, DetailsConfirm, DetailsSecurity, + Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AddTokens, AddCustomToken, WalletConnectSessions, QrScan diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index 3da94dc568..f5281cc796 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -5,6 +5,7 @@ import android.content.Intent import com.google.android.gms.wallet.PaymentData import com.shopify.buy3.Storefront import com.tangem.tap.common.analytics.AnalyticsHandler +import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct import com.tangem.tap.common.shop.data.TotalSum @@ -21,7 +22,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { private val shopifyService = ShopifyService(application, shopifyShop) - private lateinit var product: Storefront.Product private val checkouts = mutableMapOf() private val variants = mutableMapOf() @@ -31,22 +31,19 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { suspend fun getProducts(): Result> { val result = shopifyService.getProducts() - result.onSuccess { - product = it.first { - val variantsSku = it.variants.edges.map { it.node.sku } - variantsSku.contains(ProductType.WALLET_2_CARDS.sku) && variantsSku.contains( - ProductType.WALLET_3_CARDS.sku - ) - } - product.variants.edges.map { it.node } - .forEach { variant -> - if (variant.sku == ProductType.WALLET_2_CARDS.sku) { - variants[ProductType.WALLET_2_CARDS] = variant - } else if (variant.sku == ProductType.WALLET_3_CARDS.sku) { - variants[ProductType.WALLET_3_CARDS] = variant - } - } + return result.mapCatching { product -> + val availableVariants = product + .flatMap { it.variants.edges.map { it.node } } + .filter { SKUS_TO_DISPLAY.contains(it.sku) } + .associateBy { ProductType.fromSku(it.sku) } + .filterNotNull() + variants.putAll(availableVariants) + if (variants.size < SKUS_TO_DISPLAY.size) { + return Result.failure(Exception( + "Shopify: products are missing, " + + "\nproducts available: ${variants.keys.map { it.sku }}")) + } val twoCardsProduct = TangemProduct( type = ProductType.WALLET_2_CARDS, @@ -65,7 +62,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { createCheckouts() return Result.success(listOf(twoCardsProduct, threeCardsProduct)) } - return Result.failure(result.exceptionOrNull()!!) } private suspend fun createCheckouts() { @@ -214,6 +210,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { companion object { const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2" const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3" + val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) } } diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt index 89116f8096..d8db451147 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt @@ -4,5 +4,15 @@ import com.tangem.tap.common.shop.TangemShopService enum class ProductType(val sku: String) { WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU), - WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU) + WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU); + + companion object { + fun fromSku(sku: String): ProductType? { + return when (sku) { + WALLET_2_CARDS.sku -> WALLET_2_CARDS + WALLET_3_CARDS.sku -> WALLET_3_CARDS + else -> null + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt new file mode 100644 index 0000000000..8cd6e7e5ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt @@ -0,0 +1,59 @@ +package com.tangem.tap.common.ui + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.store +import com.tangem.wallet.R + +class SimpleAlertDialog { + companion object { + fun create( + titleRes: Int? = null, + messageRes: Int? = null, + title: String? = null, + message: String? = null, + primaryButtonRes: Int = R.string.common_ok, + context: Context, + ): AlertDialog { + return SimpleCancelableAlertDialog.create( + titleRes = titleRes, + messageRes = messageRes, + title = title, + message = message, + primaryButtonRes = primaryButtonRes, + secondaryButtonRes = null, + context = context + ) + } + } +} + +class SimpleCancelableAlertDialog { + companion object { + fun create( + titleRes: Int? = null, + messageRes: Int? = null, + title: String? = null, + message: String? = null, + primaryButtonRes: Int = R.string.common_ok, + secondaryButtonRes: Int? = R.string.common_cancel, + primaryButtonAction: () -> Unit = {}, + secondaryButtonAction: () -> Unit = {}, + context: Context, + ): AlertDialog { + + return AlertDialog.Builder(context).apply { + setTitle(titleRes?.let { context.getString(it) } ?: title ) + setMessage(messageRes?.let { context.getString(it) } ?: message) + setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() } + if (secondaryButtonRes != null) { + setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction()} + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt new file mode 100644 index 0000000000..66569ee1b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.common.zendesk + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ZendeskConfig( + @Json(name = "zendeskApiKey") + val apiKey: String, + @Json(name = "zendeskAppId") + val appId: String, + @Json(name = "zendeskClientId") + val clientId: String, + @Json(name = "zendeskAccountKey") + val accountKey: String, + @Json(name = "zendeskUrl") + val url: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt index 4c9cafbdee..51252c819c 100644 --- a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain import com.tangem.common.services.Result import com.tangem.domain.common.ThrottlerWithValues import com.tangem.network.api.tangemTech.TangemTechService -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import java.math.BigDecimal 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 61dbbe7f32..4833079ad0 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -19,6 +19,7 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.ScanResponse import com.tangem.operations.CommandResponse +import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.pins.CheckUserCodesCommand @@ -144,6 +145,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co 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))) + } + suspend fun runTaskAsync( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, ): CompletionResult = 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 20d1eda08c..9e22833682 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt @@ -5,44 +5,25 @@ import com.tangem.TangemSdk import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.Wallet import com.tangem.common.CompletionResult -import com.tangem.tap.domain.tasks.SignHashTask +import com.tangem.common.card.Card import com.tangem.tap.domain.tasks.SignHashesTask import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class TangemSigner( + private val card: Card, private val tangemSdk: TangemSdk, private val initialMessage: Message, private val signerCallback: (TangemSignerResponse) -> Unit, ) : TransactionSigner { - override suspend fun sign(hash: ByteArray, cardId: String, publicKey: Wallet.PublicKey): CompletionResult { + override suspend fun sign( + hashes: List, + publicKey: Wallet.PublicKey + ): CompletionResult> { return suspendCoroutine { continuation -> - val command = SignHashTask(hash, publicKey) - tangemSdk.startSessionWithRunnable( - runnable = command, - cardId = cardId, - initialMessage = initialMessage, - ) { result -> - when (result) { - is CompletionResult.Success -> { - signerCallback( - TangemSignerResponse( - result.data.totalSignedHashes, - result.data.remainingSignatures - ) - ) - continuation.resume(CompletionResult.Success(result.data.signature)) - } - is CompletionResult.Failure -> - continuation.resume(CompletionResult.Failure(result.error)) - } - } - } - } + val cardId = if (card.backupStatus?.isActive == true) null else card.cardId - override suspend fun sign(hashes: List, cardId: String, publicKey: Wallet.PublicKey): CompletionResult> { - return suspendCoroutine { continuation -> val task = SignHashesTask(hashes, publicKey) tangemSdk.startSessionWithRunnable( runnable = task, @@ -65,6 +46,21 @@ class TangemSigner( } } } + + override suspend fun sign( + hash: ByteArray, + publicKey: Wallet.PublicKey + ): CompletionResult { + val result = sign( + hashes = listOf(hash), + publicKey = publicKey + ) + + return when (result) { + is CompletionResult.Success -> CompletionResult.Success(result.data.first()) + is CompletionResult.Failure -> CompletionResult.Failure(result.error) + } + } } data class TangemSignerResponse( 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 0c38cd5a46..9d3cc79eee 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -39,21 +39,22 @@ sealed class TapError( object DustChange : TapError(R.string.send_error_dust_change) data class CreateAccountUnderfunded(override val args: List) : TapError(R.string.send_error_no_target_account) - sealed class XmlError { - object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm) - } + data class UnsupportedState( + val stateError: String, + val customMessage: String = "Unsupported state:" + ) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError")) sealed class WalletManager { - object CreationError: CustomError("Can't create wallet manager") - class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount) - class InternalError(message: String): CustomError(message) - object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable) - object BlockchainIsUnreachableTryLater: TapError(R.string.wallet_balance_blockchain_unreachable_try_later) + object CreationError : CustomError("Can't create wallet manager") + class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount) + class InternalError(message: String) : CustomError(message) + object BlockchainIsUnreachable : TapError(R.string.wallet_balance_blockchain_unreachable) + object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later) } data class ValidateTransactionErrors( - override val errorList: List, - override val builder: (List) -> String + override val errorList: List, + 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 cce3a27c15..8bd8cf8ef7 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,6 +1,12 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.Token +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.services.Result import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isStart2Coin @@ -26,7 +32,7 @@ import kotlinx.coroutines.withContext class TapWalletManager { val walletManagerFactory: WalletManagerFactory - by lazy { WalletManagerFactory(blockchainSdkConfig) } + by lazy { WalletManagerFactory(blockchainSdkConfig) } val rates: RatesRepository = RatesRepository() @@ -34,10 +40,10 @@ class TapWalletManager { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } - private val walletManagersThrottler = ThrottlerWithValues>(10000) + private val walletManagersThrottler = + ThrottlerWithValues>(10000) suspend fun loadWalletData(walletManager: WalletManager) { - val blockchain = walletManager.wallet.blockchain val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) { walletManagersThrottler.geValue(blockchainNetwork)!! @@ -54,17 +60,21 @@ class TapWalletManager { is Result.Failure -> { when (result.error) { is TapError.WalletManager.NoAccountError -> { - dispatchOnMain(WalletAction.LoadWallet.NoAccount( - walletManager.wallet, - blockchainNetwork, - (result.error as TapError.WalletManager.NoAccountError).customMessage - )) + dispatchOnMain( + WalletAction.LoadWallet.NoAccount( + walletManager.wallet, + blockchainNetwork, + (result.error as TapError.WalletManager.NoAccountError).customMessage + ) + ) } else -> { - dispatchOnMain(WalletAction.LoadWallet.Failure( - walletManager.wallet, - result.error.localizedMessage - )) + dispatchOnMain( + WalletAction.LoadWallet.Failure( + walletManager.wallet, + result.error.localizedMessage + ) + ) } } } @@ -77,10 +87,16 @@ class TapWalletManager { updateConfigManager(data) withMainContext { - store.dispatch(WalletAction.ResetState) + store.dispatch(WalletAction.ResetState(data.card.cardId)) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard)) store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) + store.dispatch( + WalletAction.MultiWallet.ShowWalletBackupWarning( + show = data.card.settings.isBackupAllowed + && data.card.backupStatus == Card.BackupStatus.NoBackup + ) + ) loadData(data) } } @@ -109,6 +125,37 @@ class TapWalletManager { return } + if (data.card.isMultiwalletAllowed) { + loadMultiWalletData(data) + } else { + loadSingleWalletData(data) + } + + dispatchOnMain(WalletAction.LoadWallet()) + dispatchOnMain(WalletAction.LoadFiatRate()) + } + + private suspend fun loadMultiWalletData( + scanResponse: ScanResponse + ) { + val savedCurrencies = currenciesRepository.loadSavedCurrencies( + scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed + ) + if (savedCurrencies.isEmpty()) return + + val walletManagers = + walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies) + dispatchOnMain( + WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers), + ) + savedCurrencies.map { + if (it.tokens.isNotEmpty()) { + dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it)) + } + } + } + + private suspend fun loadSingleWalletData(data: ScanResponse) { val blockchain = data.getBlockchain() val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data) @@ -120,87 +167,12 @@ class TapWalletManager { primaryWalletManager.addToken(primaryToken) dispatchOnMain(WalletAction.MultiWallet.SetPrimaryToken(primaryToken)) } - if (data.card.isMultiwalletAllowed) { - loadMultiWalletData(data, blockchain, primaryWalletManager) - } else { - dispatchOnMain(WalletAction.MultiWallet.AddBlockchains( + dispatchOnMain( + WalletAction.MultiWallet.AddBlockchains( listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)), listOf(primaryWalletManager) - )) - } - } else { - if (data.card.isMultiwalletAllowed) { - loadMultiWalletData(data, blockchain, null) - } - } - dispatchOnMain(WalletAction.LoadWallet()) - dispatchOnMain(WalletAction.LoadFiatRate()) - } - - private suspend fun loadMultiWalletData( - scanResponse: ScanResponse, - primaryBlockchain: Blockchain?, - primaryWalletManager: WalletManager? - ) { - val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList() - val savedCurrencies = currenciesRepository.loadSavedCurrencies( - scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed - ) - - if (savedCurrencies.isEmpty()) { - if (primaryBlockchain != null && primaryWalletManager != null) { - val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager) - dispatchOnMain( - WalletAction.MultiWallet.SaveCurrencies(listOf(blockchainNetwork)), - WalletAction.MultiWallet.AddBlockchains( - listOf(blockchainNetwork), - listOf(primaryWalletManager) - ), - WalletAction.MultiWallet.AddTokens(primaryTokens.toList(), blockchainNetwork) ) - - } else { - val blockchainNetworks = listOf( - BlockchainNetwork(Blockchain.Bitcoin, scanResponse.card), - BlockchainNetwork(Blockchain.Ethereum, scanResponse.card) - ) - - val walletManagers = walletManagerFactory.makeWalletManagersForApp( - scanResponse, - blockchainNetworks - ) - - dispatchOnMain( - WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks), - WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers), - ) - } -// dispatchOnMain( -// WalletAction.MultiWallet.FindBlockchainsInUse, -// WalletAction.MultiWallet.FindTokensInUse, -// ) - } else { - val walletManagers = if ( - primaryTokens.isNotEmpty() && - primaryWalletManager != null && - primaryBlockchain != null && primaryBlockchain != Blockchain.Unknown - ) { - val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain } - walletManagerFactory.makeWalletManagersForApp( - scanResponse, - blockchainsWithoutPrimary - ).plus(primaryWalletManager) - } else { - walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies) - } - dispatchOnMain( - WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers), ) - savedCurrencies.map { - if (it.tokens.isNotEmpty()) { - dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it)) - } - } } } 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 bd99957d6e..bb94004636 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 @@ -1,8 +1,8 @@ package com.tangem.tap.domain.configurable.config import com.tangem.blockchain.common.BlockchainSdkConfig -import com.tangem.common.extensions.VoidCallback import com.tangem.tap.common.shop.shopify.ShopifyShop +import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.configurable.Loader /** @@ -20,7 +20,8 @@ data class Config( val isTopUpEnabled: Boolean = false, @Deprecated("Not relevant since version 3.23") val isCreatingTwinCardsAllowed: Boolean = false, - val shopify: ShopifyShop? = null + val shopify: ShopifyShop? = null, + val zendesk: ZendeskConfig? = null, ) class ConfigManager( @@ -33,11 +34,11 @@ class ConfigManager( private var defaultConfig = Config() - fun load(onComplete: VoidCallback? = null) { - localLoader.load { config -> - setupFeature(config.features) - setupKey(config.configValues) - onComplete?.invoke() + fun load(onComplete: ((config: Config) -> Unit)? = null) { + localLoader.load { configModel -> + setupFeature(configModel.features) + setupKey(configModel.configValues) + onComplete?.invoke(config) } // Uncomment to enable remote config // remoteLoader.load { config -> @@ -95,6 +96,7 @@ class ConfigManager( ), appsFlyerDevKey = values.appsFlyerDevKey, shopify = values.shopifyShop, + zendesk = values.zendesk, ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, @@ -110,6 +112,7 @@ class ConfigManager( ), appsFlyerDevKey = values.appsFlyerDevKey, shopify = values.shopifyShop, + zendesk = values.zendesk, ) } 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 542a006a8e..6e119fc5e6 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 @@ -1,6 +1,7 @@ package com.tangem.tap.domain.configurable.config import com.tangem.tap.common.shop.shopify.ShopifyShop +import com.tangem.tap.common.zendesk.ZendeskConfig /** [REDACTED_AUTHOR] @@ -25,6 +26,7 @@ class ConfigValueModel( val infuraProjectId: String?, val appsFlyerDevKey: String, val shopifyShop: ShopifyShop?, + val zendesk: ZendeskConfig?, val tronGridApiKey: String, ) diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index ac2d922c0d..10dbcb2cbb 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -6,7 +6,6 @@ import com.tangem.tap.common.extensions.containsAny import com.tangem.tap.common.extensions.removeBy import com.tangem.wallet.BuildConfig import com.tangem.wallet.R -import java.util.* /** [REDACTED_AUTHOR] @@ -188,31 +187,6 @@ class WarningMessagesManager( WarningMessage.Origin.Local ) - fun restoreFundsWarning(): WarningMessage = WarningMessage( - title = "", - message = "", - type = WarningMessage.Type.Temporary, - priority = WarningMessage.Priority.Warning, - listOf(WarningMessage.Location.MainScreen), - blockchains = null, - titleResId = R.string.alert_title, - messageResId = R.string.alert_funds_restoration_message, - origin = WarningMessage.Origin.Local, - buttonTextId = R.string.warning_button_learn_more - ) - const val REMAINING_SIGNATURES_WARNING = 10 - private const val RESTORE_FUNDS_GUIDE_URL_RU = - "https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet" - private const val RESTORE_FUNDS_GUIDE_URL_EN = - "https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet" - - fun getRestoreFundsGuideUrl(locale: String): String { - return if (locale == Locale("ru").language) { - RESTORE_FUNDS_GUIDE_URL_RU - } else { - RESTORE_FUNDS_GUIDE_URL_EN - } - } } } \ No newline at end of file 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 0dc1819018..c627a7de91 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,8 +6,8 @@ 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 import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTangemNote import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.common.isTangemTwin @@ -15,7 +15,6 @@ import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.features.wallet.redux.Artwork - val Card.remainingSignatures: Int? get() = this.getSingleWallet()?.remainingSignatures @@ -24,7 +23,7 @@ val Card.isWalletDataSupported: Boolean val Card.isMultiwalletAllowed: Boolean get() { - return !isTangemTwin() && !isStart2Coin && !TapWorkarounds.isTangemNote(this) + return !isTangemTwin() && !isStart2Coin && !isTangemNote() && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || getSingleWallet()?.curve == EllipticCurve.Secp256k1) } diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index bff4c0c3b7..34b8289884 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 @@ -11,7 +11,7 @@ import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, @@ -32,9 +32,10 @@ fun WalletManagerFactory.makeWalletManagerForApp( return when { scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { makeTwinWalletManager( - card.cardId, - wallet.publicKey, scanResponse.secondTwinPublicKey!!.hexToBytes(), - environmentBlockchain, wallet.curve + walletPublicKey = wallet.publicKey, + pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(), + blockchain = environmentBlockchain, + curve = wallet.curve ) } seedKey != null && derivationParams != null -> { @@ -47,7 +48,6 @@ fun WalletManagerFactory.makeWalletManagerForApp( ?: return null makeWalletManager( - cardId = card.cardId, blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, @@ -56,7 +56,6 @@ fun WalletManagerFactory.makeWalletManagerForApp( } else -> { makeWalletManager( - cardId = card.cardId, blockchain = environmentBlockchain, walletPublicKey = wallet.publicKey, curve = wallet.curve diff --git a/app/src/main/java/com/tangem/tap/domain/statePrinter/WalletStateConverter.kt b/app/src/main/java/com/tangem/tap/domain/statePrinter/WalletStateConverter.kt index 07ac4886bb..57762aaf51 100644 --- a/app/src/main/java/com/tangem/tap/domain/statePrinter/WalletStateConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/statePrinter/WalletStateConverter.kt @@ -57,7 +57,6 @@ class WalletStateConverter : StringStateConverter { walletMap["publicKey"] = publicKeyMap walletMap["amounts"] = amounts walletMap["addresses"] = wallet.addresses.toString() - walletMap["cardId"] = wallet.cardId return walletMap } 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 d57336841e..e54c3744cb 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 @@ -14,10 +14,14 @@ import com.tangem.common.extensions.guard import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath -import com.tangem.domain.common.* +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.isTangemNote 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 @@ -56,7 +60,7 @@ class ScanProductTask( } val commandProcessor = when { - TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor() + card.isTangemNote() -> ScanNoteProcessor() card.isTangemTwins() -> ScanTwinProcessor() else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive) } @@ -64,16 +68,15 @@ class ScanProductTask( when (processorResult) { is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult -> when (scanTaskResult) { - is CompletionResult.Success -> callback( - CompletionResult.Success( - processorResult.data + is CompletionResult.Success -> { + // it need because processorResult.data.card doesn't contains attestation result + // and CardWallet.derivedKeys + val processorScanResponseWithNewCard = processorResult.data.copy( + card = scanTaskResult.data ) - ) - is CompletionResult.Failure -> callback( - CompletionResult.Failure( - scanTaskResult.error - ) - ) + callback(CompletionResult.Success(processorScanResponseWithNewCard)) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error)) } } is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error)) @@ -98,11 +101,11 @@ private class ScanNoteProcessor : ProductCommandProcessor { callback( CompletionResult.Success( ScanResponse( - card, - ProductType.Note, - session.environment.walletData - ) - ) + card = card, + productType = ProductType.Note, + walletData = session.environment.walletData, + ), + ), ) } } @@ -113,19 +116,17 @@ private class ScanWalletProcessor( ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null - override fun proceed( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { createMissingWalletsIfNeeded(card, session, callback) } - private fun createMissingWalletsIfNeeded( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { startLinkingForBackupIfNeeded(card, session, callback) @@ -142,18 +143,12 @@ private class ScanWalletProcessor( when (result) { is CompletionResult.Success -> { PreflightReadTask( - PreflightReadMode.FullCardRead, - card.cardId + readMode = PreflightReadMode.FullCardRead, + cardId = card.cardId ).run(session) { readResult -> when (readResult) { - is CompletionResult.Success -> { - startLinkingForBackupIfNeeded(card, session, callback) - } - is CompletionResult.Failure -> callback( - CompletionResult.Failure( - readResult.error - ) - ) + is CompletionResult.Success -> startLinkingForBackupIfNeeded(card, session, callback) + is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) } } } @@ -161,18 +156,14 @@ private class ScanWalletProcessor( } } } - private fun startLinkingForBackupIfNeeded( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { - val activationIsFinished = - preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId) + val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId) - if (card.backupStatus == Card.BackupStatus.NoBackup && - !activationIsFinished && card.wallets.isNotEmpty() - ) { + if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) { StartPrimaryCardLinkingTask().run(session) { linkingResult -> when (linkingResult) { is CompletionResult.Success -> { @@ -188,11 +179,10 @@ private class ScanWalletProcessor( deriveKeysIfNeeded(card, session, callback) } } - private fun deriveKeysIfNeeded( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { scope.launch { val derivations = collectDerivations(card) @@ -231,12 +221,13 @@ private class ScanWalletProcessor( private suspend fun getBlockchainsToDerive(card: Card): List { val currenciesRepository = currenciesRepository ?: return emptyList() - val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList() + val cardCurrencies = currenciesRepository + .loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList() val blockchainsToDerive = cardCurrencies.ifEmpty { mutableListOf( BlockchainNetwork(Blockchain.Bitcoin, card), - BlockchainNetwork(Blockchain.Ethereum, card) + BlockchainNetwork(Blockchain.Ethereum, card), ) } @@ -244,7 +235,7 @@ private class ScanWalletProcessor( blockchainsToDerive.addAll( listOf( BlockchainNetwork(Blockchain.Ethereum, card), - BlockchainNetwork(Blockchain.EthereumTestnet, card) + BlockchainNetwork(Blockchain.EthereumTestnet, card), ) ) } @@ -301,52 +292,44 @@ private class ScanTwinProcessor : ProductCommandProcessor { is CompletionResult.Success -> { val publicKey = card.getSingleWallet()?.publicKey if (publicKey == null) { - callback( - CompletionResult.Success( - ScanResponse( - card, - ProductType.Twins, - null - ) - ) - ) - return@run - } - val verified = - TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey) - if (verified) { - val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65) - val walletData = session.environment.walletData val response = ScanResponse( - card, - ProductType.Twins, - walletData, - twinPublicKey.toHexString() + card = card, + productType = ProductType.Twins, + walletData = null, ) callback(CompletionResult.Success(response)) + return@run + } + + val verified = TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey) + val response = if (verified) { + val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65) + val walletData = session.environment.walletData + ScanResponse( + card = card, + productType = ProductType.Twins, + walletData = walletData, + secondTwinPublicKey = twinPublicKey.toHexString(), + ) } else { - callback( - CompletionResult.Success( - ScanResponse( - card, - ProductType.Twins, - null - ) - ) + ScanResponse( + card = card, + productType = ProductType.Twins, + walletData = null, ) } + callback(CompletionResult.Success(response)) } - is CompletionResult.Failure -> + is CompletionResult.Failure -> { callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null))) + } } } } - } fun Card.getCurvesForNonCreatedWallets(): List { val curvesPresent = wallets.map { it.curve }.toSet() - val curvesForNonCreatedWallets = supportedCurves - .subtract(curvesPresent + EllipticCurve.Secp256r1) + val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1) return curvesForNonCreatedWallets.toList() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt index 032c0b97ca..f05065bc8d 100644 --- a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt +++ b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt @@ -4,16 +4,17 @@ import android.content.res.Resources import android.net.Uri import androidx.core.os.ConfigurationCompat import com.tangem.common.card.Card -import java.util.* +import java.util.Locale /** [REDACTED_AUTHOR] */ class CardTou { - private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0) + private val locale: Locale = + ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!! fun getUrl(card: Card): Uri? { - val issuerName = card.issuer.name ?: return null + val issuerName = card.issuer.name if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null val baseUrl = "https://app.tangem.com/tou/" diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt index fe8a593787..b366229775 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/EthSignHelper.kt @@ -1,11 +1,13 @@ package com.tangem.tap.domain.walletconnect import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.toMap import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonParser import com.google.gson.annotations.SerializedName +import com.tangem.blockchain.common.Blockchain import com.trustwallet.walletconnect.JSONRPC_VERSION import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage @@ -18,9 +20,11 @@ class EthSignHelper { .create() } - fun tryToParseEthTypedMessage(data: String): WCEthereumSignMessage? { - val request = - gson.fromJson(data) + fun parseCustomRequest(data: String): CustomJsonRpcRequest { + return gson.fromJson(data) + } + + fun tryToParseEthTypedMessage(request: CustomJsonRpcRequest): WCEthereumSignMessage? { return if (request.method == WCMethodExtended.ETH_SIGN_TYPE_DATA_V4) { WCEthereumSignMessage( listOf( @@ -53,12 +57,26 @@ class EthSignHelper { } } -data class CustomJsonRpcRequests( +data class CustomJsonRpcRequest( val id: Long, val jsonrpc: String = JSONRPC_VERSION, val method: WCMethodExtended?, val params: JsonArray -) +) { + + fun blockchainFromChainId(): Blockchain? { + return try { + val hex = params[0].asJsonObject.toMap()[CHAIN_ID_KEY]?.asString ?: "" + Blockchain.fromChainId(Integer.decode(hex)) + } catch (exception: Exception) { + null + } + } + + companion object { + const val CHAIN_ID_KEY = "chainId" + } +} enum class WCMethodExtended { @SerializedName("wc_sessionRequest") @@ -95,5 +113,10 @@ enum class WCMethodExtended { GET_ACCOUNTS, @SerializedName("trust_signTransaction") - SIGN_TRANSACTION; + SIGN_TRANSACTION, + + @SerializedName("wallet_switchEthereumChain") + SWITCH_CHAIN, + + ; } \ No newline at end of file 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 dd94266b06..7e4fae297a 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 @@ -5,20 +5,32 @@ import com.tangem.common.extensions.guard import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.* +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 +import com.tangem.tap.features.details.redux.walletconnect.WalletForSession +import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.walletConnectRepository import com.trustwallet.walletconnect.WCClient import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.binance.* +import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder +import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder +import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder +import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction import com.trustwallet.walletconnect.models.session.WCSession import com.trustwallet.walletconnect.models.session.WCSessionUpdate import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import okhttp3.* +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber import java.util.* @@ -71,6 +83,22 @@ class WalletConnectManager { } } + fun updateBlockchain(session: WalletConnectSession) { + sessions[session.session]?.client?.updateSession( + accounts = listOfNotNull(session.getAddress()), + chainId = session.wallet.blockchain?.getChainId(), + approved = true + ) + + + val updatedSession = sessions[session.session]?.copy( + wallet = session.wallet + ) + if (updatedSession != null) { + sessions[session.session] = updatedSession + } + } + private fun setupConnectionTimeoutCheck(session: WCSession) { scope.launch { delay(20_000) @@ -405,27 +433,47 @@ class WalletConnectManager { onSessionClosed(session) } } + client.onWalletChangeNetwork = { id: Long, chainId: Int -> + Timber.d("On WC Switch Chain") + val blockchain = Blockchain.fromChainId(chainId) + Timber.d("WC switch chainID\nNew Blockchain: $blockchain") + val session = sessions[client.session]?.toWalletConnectSession() + if (session != null) { + store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session)) + } + } client.onCustomRequest = { id: Long, data: String -> Timber.d("Custom Request") Timber.d(data) + val request = EthSignHelper.parseCustomRequest(data) - val message = EthSignHelper.tryToParseEthTypedMessage(data) - if (message != null) { - Timber.d("onEthSign_v4: $message") - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Action( - Analytics.WcAction.PersonalSign + when (request.method) { + WCMethodExtended.ETH_SIGN_TYPE_DATA_V4 -> handleTypedDataV4(request, client, id) + else -> { + Timber.d("WC: unrecognized custom request") + } + } + } + + } + + private fun handleTypedDataV4(request: CustomJsonRpcRequest, client: WCClient, id: Long) { + val message = EthSignHelper.tryToParseEthTypedMessage(request) + if (message != null) { + Timber.d("onEthSign_v4: $message") + store.state.globalState.analyticsHandlers?.logWcEvent( + Analytics.WcAnalyticsEvent.Action( + Analytics.WcAction.PersonalSign + ) + ) + sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + store.dispatchOnMain( + WalletConnectAction.HandlePersonalSignRequest( + message, + sessionData, + id ) ) - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandlePersonalSignRequest( - message, - sessionData, - id - ) - ) - } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt index aba7726098..3da00c517b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt @@ -28,11 +28,15 @@ class WalletConnectNetworkUtils { peer.name.contains("BSC") -> { Blockchain.BSC } + peer.url.contains("honeyswap.1hive.eth.limo") -> { + // Check if something's changed after this bug report: + // https://github.com/1Hive/honeyswap-interface/issues/83 + Blockchain.Gnosis + } else -> { - Blockchain.Ethereum + Blockchain.Ethereum } } } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 1d0dc4a9e0..04f1ec8141 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -5,8 +5,18 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak -import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.CommonSigner +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.hexToBigDecimal +import com.tangem.blockchain.extensions.isAscii import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes @@ -15,9 +25,14 @@ import com.tangem.common.extensions.toHexString import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.extensions.logSendTransactionError import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.features.details.redux.walletconnect.* +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.features.details.redux.walletconnect.WalletForSession +import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData import com.tangem.tap.store @@ -56,7 +71,7 @@ class WalletConnectSdkHelper { (walletManager as? EthereumGasLoader)?.getGasPrice()) { is Result.Success -> result.data.toBigDecimal() is Result.Failure -> { - Timber.e(result.error) + (result.error as? Throwable)?.let { Timber.e(it) } return null } null -> return null @@ -108,9 +123,8 @@ class WalletConnectSdkHelper { ) val blockchain = session.wallet.getBlockchainForSession() return factory.makeWalletManager( - session.wallet.cardId, - blockchain, - publicKey + blockchain = blockchain, + publicKey = publicKey ) } @@ -124,20 +138,18 @@ class WalletConnectSdkHelper { private suspend fun sendTransaction(data: WcTransactionData): String? { val result = (data.walletManager as TransactionSender).send( transactionData = data.transaction, - signer = Signer(tangemSdk) + signer = CommonSigner(tangemSdk) ) return when (result) { SimpleResult.Success -> { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( - error, - Analytics.ActionToLog.WalletConnectTransaction, - ) - } - Timber.e(result.error) + store.state.globalState.analyticsHandlers?.logSendTransactionError( + result.error, + Analytics.ActionToLog.WalletConnectTransaction, + ) + Timber.e(result.error as BlockchainSdkError) null } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WcWalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WcWalletManagerFactory.kt new file mode 100644 index 0000000000..72134d880b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WcWalletManagerFactory.kt @@ -0,0 +1,94 @@ +package com.tangem.tap.domain.walletconnect + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.tap.domain.extensions.getPrimaryCurve +import com.tangem.tap.domain.extensions.makeWalletManagerForApp +import com.tangem.tap.domain.tokens.CurrenciesRepository +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.details.redux.walletconnect.WalletForSession +import com.tangem.tap.features.wallet.redux.WalletState + +class WcWalletManagerFactory( + private val factory: WalletManagerFactory, + private val currenciesRepository: CurrenciesRepository, + ) { + + suspend fun getWalletManager( + wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState + ): WalletManager? { + val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) { + Blockchain.EthereumTestnet + } else { + blockchain + } + + val blockchainNetwork = BlockchainNetwork( + blockchain = blockchainToMake, + derivationPath = wallet.derivationPath?.rawPath, + tokens = emptyList() + ) + + return if (walletState.cardId == wallet.cardId) { + walletState.getWalletManager(blockchainNetwork) + } else { + val blockchainNetworkWithTokens = currenciesRepository + .loadSavedCurrencies( + cardId = wallet.cardId, + isHdWalletSupported = wallet.derivationPath != null + ).firstOrNull { it == blockchainNetwork } + + if (blockchainNetworkWithTokens != null) { + factory.makeWalletManager( + blockchain = blockchainToMake, + publicKey = Wallet.PublicKey( + wallet.walletPublicKey!!, + wallet.derivedPublicKey, + wallet.derivationPath + ), + tokens = blockchainNetworkWithTokens.tokens, + curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1 + ) + } else { + null + } + } + } + + suspend fun getWalletManager( + scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState + ): WalletManager? { + val card = scanResponse.card + val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) { + Blockchain.EthereumTestnet + } else { + blockchain + } + + val blockchainNetwork = BlockchainNetwork( + blockchain = blockchainToMake, + card = card + ) + + return if (walletState.cardId == card.cardId) { + walletState.getWalletManager(blockchainNetwork) + } else { + if (currenciesRepository + .loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed) + .contains(blockchainNetwork) + ) { + factory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchainNetwork = blockchainNetwork + ) + } else { + null + } + } + } +} \ No newline at end of file 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 2a38c4e657..650ce5ea09 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 @@ -1,13 +1,20 @@ package com.tangem.tap.features.demo import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.toBlockchainSdkError import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.wallet.redux.WalletAction @@ -36,7 +43,8 @@ object DemoHelper { WalletAction.TradeCryptoAction.Buy::class.java, WalletAction.TradeCryptoAction.Sell::class.java, BackupAction.StartBackup::class.java, - WalletAction.ExploreAddress::class.java + WalletAction.ExploreAddress::class.java, + DetailsAction.ResetToFactory.Start::class.java, ) fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) @@ -471,9 +479,12 @@ class DemoTransactionSender( override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { val dataToSign = randomString(32).toByteArray() - val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey) + val signerResponse = signer.sign( + hash = dataToSign, + publicKey = walletManager.wallet.publicKey + ) return when (signerResponse) { - is CompletionResult.Success -> SimpleResult.Failure(Exception(ID)) + is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) } } diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt index 92551d6411..87af003060 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt @@ -5,7 +5,7 @@ import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.scope import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt index 99ecfbc227..66d09bdbf0 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.demo -import com.tangem.blockchain.common.WalletManager +import com.tangem.common.card.Card import com.tangem.domain.common.ScanResponse /** [REDACTED_AUTHOR] */ fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId) -fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId) \ No newline at end of file +fun Card.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index c37a625c54..b421b69730 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -3,12 +3,8 @@ package com.tangem.tap.features.details.redux import com.tangem.blockchain.common.Wallet import com.tangem.common.card.Card import com.tangem.domain.common.ScanResponse -import com.tangem.domain.common.TwinCardNumber -import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.termsOfUse.CardTou -import com.tangem.wallet.R import org.rekotlin.Action sealed class DetailsAction : Action { @@ -20,31 +16,24 @@ sealed class DetailsAction : Action { ) : DetailsAction() object ShowDisclaimer : DetailsAction() - data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction() + object ReCreateTwinsWallet : DetailsAction() sealed class ResetToFactory : DetailsAction() { - object Check : ResetToFactory() - object Proceed : ResetToFactory() { - object NotAllowedByCard : ResetToFactory(), NotificationAction { - override val messageResource = R.string.error_purge_prohibited - } - - object NotEmpty : ResetToFactory(), NotificationAction { - override val messageResource = R.string.details_notification_erase_wallet_not_possible - } - } - - object Confirm : ResetToFactory() - object Cancel : ResetToFactory() + object Start : ResetToFactory() + object Proceed : ResetToFactory() + data class Confirm(val confirmed: Boolean) : ResetToFactory() object Failure : ResetToFactory() object Success : ResetToFactory() } object CreateBackup : DetailsAction() + object ScanCard : DetailsAction() + + data class PrepareCardSettingsData(val card: Card) : DetailsAction() + object ResetCardSettingsData : DetailsAction() + sealed class ManageSecurity : DetailsAction() { - data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity() - data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity() object OpenSecurity : ManageSecurity() data class SelectOption(val option: SecurityOption) : ManageSecurity() object SaveChanges : ManageSecurity() { @@ -52,11 +41,12 @@ sealed class DetailsAction : Action { object Failure : ManageSecurity() } - data class ConfirmSelection(val option: SecurityOption) : ManageSecurity() { - object AlreadySet : ManageSecurity(), NotificationAction { - override val messageResource = R.string.details_notification_security_option_already_active - } - } + object ChangeAccessCode : ManageSecurity() + } + + sealed class AppSettings : DetailsAction() { + data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) : + AppSettings() } data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction() 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 efed01f9c3..7825d58a8f 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 @@ -1,9 +1,8 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.TangemSdkError -import com.tangem.operations.pins.CheckUserCodesResponse +import com.tangem.domain.common.isTangemTwins import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.extensions.dispatchNotification @@ -13,7 +12,7 @@ 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.currenciesRepository -import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions @@ -23,44 +22,74 @@ import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.rekotlin.Action import org.rekotlin.Middleware class DetailsMiddleware { private val eraseWalletMiddleware = EraseWalletMiddleware() private val manageSecurityMiddleware = ManageSecurityMiddleware() - val detailsMiddleware: Middleware = { _, _ -> + private val managePrivacyMiddleware = ManagePrivacyMiddleware() + val detailsMiddleware: Middleware = { _, state -> { next -> { action -> - when (action) { - is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) - is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) - is DetailsAction.ShowDisclaimer -> { - store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + handleAction(state, action) + next(action) + } + } + } + + private fun handleAction(state: () -> AppState?, action: Action) { + if (DemoHelper.tryHandle(state, action)) return + + when (action) { + is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) + is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) + is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action) + is DetailsAction.ShowDisclaimer -> { + val uri = store.state.detailsState.cardTermsOfUseUrl + if (uri != null) { + store.dispatch(NavigationAction.OpenDocument(uri)) + } + } + is DetailsAction.ReCreateTwinsWallet -> { + val wallet = + store.state.walletState.walletManagers.map { it.wallet }.firstOrNull() + if (wallet == null) { + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) + } else { + if (wallet.hasSendableAmountsOrPendingTransactions()) { + val walletIsNotEmpty = + store.state.globalState.resources.strings.walletIsNotEmpty + store.dispatchNotification(walletIsNotEmpty) + } else { + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } - is DetailsAction.ReCreateTwinsWallet -> { - val wallet = store.state.walletState.walletManagers.map { it.wallet }.firstOrNull() - if (wallet == null) { - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) - } else { - if (wallet.hasSendableAmountsOrPendingTransactions()) { - val walletIsNotEmpty = store.state.globalState.resources.strings.walletIsNotEmpty - store.dispatchNotification(walletIsNotEmpty) - } else { - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) - } + } + } + is DetailsAction.CreateBackup -> { + store.state.detailsState.scanResponse?.let { + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + store.dispatch( + GlobalAction.Onboarding.Start( + it, + fromHomeScreen = false, + ), + ) + } + } + DetailsAction.ScanCard -> { + scope.launch { + when (val result = tangemSdkManager.scanCard()) { + is CompletionResult.Success -> { + val card = result.data + store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card)) } - } - is DetailsAction.CreateBackup -> { - store.state.detailsState.scanResponse?.let { - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) - store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false)) + is CompletionResult.Failure -> { } } } - next(action) } } } @@ -68,23 +97,15 @@ class DetailsMiddleware { class EraseWalletMiddleware { fun handle(action: DetailsAction.ResetToFactory) { when (action) { + is DetailsAction.ResetToFactory.Start -> { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory)) + } is DetailsAction.ResetToFactory.Proceed -> { - when (store.state.detailsState.eraseWalletState) { - EraseWalletState.Allowed -> - store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm)) - EraseWalletState.NotAllowedByCard -> - store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard) - EraseWalletState.NotEmpty -> - store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty) - else -> { /* no-op */ - } + val card = store.state.detailsState.cardSettingsState?.card ?: return + if (card.isTangemTwins()) { + store.dispatch(DetailsAction.ReCreateTwinsWallet) + return } - } - is DetailsAction.ResetToFactory.Cancel -> { - store.dispatch(NavigationAction.PopBackTo()) - } - is DetailsAction.ResetToFactory.Confirm -> { - val card = store.state.detailsState.scanResponse?.card ?: return scope.launch { val result = tangemSdkManager.resetToFactorySettings(card) withContext(Dispatchers.Main) { @@ -98,7 +119,7 @@ class DetailsMiddleware { store.state.globalState.analyticsHandlers?.logCardSdkError( error, Analytics.ActionToLog.PurgeWallet, - card = store.state.detailsState.scanResponse?.card + card = store.state.detailsState.scanResponse?.card, ) } } @@ -115,44 +136,13 @@ class DetailsMiddleware { class ManageSecurityMiddleware { fun handle(action: DetailsAction.ManageSecurity) { when (action) { - is DetailsAction.ManageSecurity.CheckCurrentSecurityOption -> { - if (action.card.firmwareVersion >= FirmwareVersion.IsAccessCodeStatusAvailable) { - // for a card that meets this condition, we can get these statuses from it - val simulatedResponse = CheckUserCodesResponse( - action.card.isAccessCodeSet, action.card.isPasscodeSet ?: false - ) - store.dispatch(DetailsAction.ManageSecurity.SetCurrentOption(simulatedResponse)) - store.dispatch(DetailsAction.ManageSecurity.OpenSecurity) - } else { - scope.launch { - when (val response = tangemSdkManager.checkUserCodes(action.card.cardId)) { - is CompletionResult.Success -> { - store.dispatchOnMain(DetailsAction.ManageSecurity.SetCurrentOption(response.data)) - store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity) - } - is CompletionResult.Failure -> { - } - } - } - } - } is DetailsAction.ManageSecurity.OpenSecurity -> { store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) } - is DetailsAction.ManageSecurity.ConfirmSelection -> { - if (action.option != store.state.detailsState.securityScreenState?.currentOption) { - if (action.option == SecurityOption.LongTap) { - store.dispatch(DetailsAction.ManageSecurity.SaveChanges) - } else { - store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm)) - } - } else { - store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection.AlreadySet) - } - } is DetailsAction.ManageSecurity.SaveChanges -> { val cardId = store.state.detailsState.scanResponse?.card?.cardId - val selectedOption = store.state.detailsState.securityScreenState?.selectedOption + val selectedOption = + store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption scope.launch { val result = when (selectedOption) { SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId) @@ -166,9 +156,7 @@ class DetailsMiddleware { selectedOption?.let { store.dispatch(GlobalAction.UpdateSecurityOptions(it)) } - if (selectedOption != SecurityOption.LongTap) { - store.dispatch(NavigationAction.PopBackTo()) - } + store.dispatch(NavigationAction.PopBackTo()) store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success) } is CompletionResult.Failure -> { @@ -178,9 +166,9 @@ class DetailsMiddleware { actionToLog = Analytics.ActionToLog.ChangeSecOptions, parameters = mapOf( AnalyticsParam.NEW_SECURITY_OPTION to - (selectedOption?.name ?: "") + (selectedOption?.name ?: ""), ), - card = store.state.detailsState.scanResponse?.card + card = store.state.detailsState.scanResponse?.card, ) } store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) @@ -191,9 +179,25 @@ class DetailsMiddleware { } } } + is DetailsAction.ManageSecurity.ChangeAccessCode -> { + val card = store.state.detailsState.cardSettingsState?.card ?: return + scope.launch { + tangemSdkManager.setAccessCode(card.cardId) + } + } else -> { /* no-op */ } } } } -} \ No newline at end of file + + class ManagePrivacyMiddleware { + fun handle(action: DetailsAction.AppSettings) { + when (action) { + is DetailsAction.AppSettings.SwitchPrivacySetting -> { +// TODO() + } + } + } + } +} 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 a095d46d7e..b327e24452 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 @@ -1,13 +1,14 @@ package com.tangem.tap.features.details.redux - import com.tangem.common.card.Card +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.isTangemTwin import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.isWalletDataSupported import com.tangem.tap.domain.extensions.signedHashesCount -import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions +import com.tangem.tap.store import org.rekotlin.Action import java.util.* @@ -19,69 +20,95 @@ class DetailsReducer { private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState - val detailsState = state.detailsState return when (action) { is DetailsAction.PrepareScreen -> { - handlePrepareScreen(action, detailsState) + handlePrepareScreen(action) } + is DetailsAction.PrepareCardSettingsData -> { + handlePrepareCardSettingsScreen(action.card, detailsState) + } + is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null) is DetailsAction.ResetToFactory -> { handleEraseWallet(action, detailsState) } is DetailsAction.ManageSecurity -> { handleSecurityAction(action, detailsState) } + is DetailsAction.AppSettings -> { + handlePrivacyAction(action, detailsState) + } is DetailsAction.ChangeAppCurrency -> detailsState.copy(appCurrency = action.fiatCurrency) - else -> detailsState } } private fun handlePrepareScreen( action: DetailsAction.PrepareScreen, - state: DetailsState, ): DetailsState { - return DetailsState( scanResponse = action.scanResponse, wallets = action.wallets, - cardInfo = action.scanResponse.card.toCardInfo(), cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card), createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup, + appCurrency = store.state.globalState.appCurrency, ) } +private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): DetailsState { + val cardSettingsState = CardSettingsState( + cardInfo = card.toCardInfo(), + manageSecurityState = prepareSecurityOptions(card), + card = card, + resetCardAllowed = isResetToFactoryAllowedByCard(card), + ) + return state.copy(cardSettingsState = cardSettingsState) +} + +private fun prepareSecurityOptions(card: Card): ManageSecurityState { + val securityOption = when { + card.isAccessCodeSet -> { + SecurityOption.AccessCode + } + card.isPasscodeSet == true -> { + SecurityOption.PassCode + } + else -> { + SecurityOption.LongTap + } + } + val allowedSecurityOptions = when { + card.isStart2Coin || card.isTangemNote() -> { + EnumSet.of(SecurityOption.LongTap) + } + card.settings.isBackupAllowed -> { + EnumSet.of(securityOption) + } + else -> prepareAllowedSecurityOptions(card, securityOption) + } + return ManageSecurityState( + currentOption = securityOption, + allowedOptions = allowedSecurityOptions, + selectedOption = securityOption, + ) +} + +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.isSaltPay + return !notAllowedByCard +} + private fun handleEraseWallet( action: DetailsAction.ResetToFactory, state: DetailsState, ): DetailsState { return when (action) { - DetailsAction.ResetToFactory.Check -> { - val card = state.scanResponse?.card - val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false - val notAllowedByCard = notAllowedByAnyWallet || - (card?.isWalletDataSupported == true && - (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) - - val notEmpty = state.wallets.any { it.hasSendableAmountsOrPendingTransactions() } - val eraseWalletState = when { - notAllowedByCard -> EraseWalletState.NotAllowedByCard - notEmpty -> EraseWalletState.NotEmpty - else -> EraseWalletState.Allowed - } - state.copy(eraseWalletState = eraseWalletState) - } - DetailsAction.ResetToFactory.Proceed -> { - if (state.eraseWalletState == EraseWalletState.Allowed) { - state.copy(confirmScreenState = ConfirmScreenState.EraseWallet) - } else { - state - } - } - DetailsAction.ResetToFactory.Cancel -> state.copy(eraseWalletState = null) - DetailsAction.ResetToFactory.Failure -> state.copy(eraseWalletState = null) - DetailsAction.ResetToFactory.Success -> state.copy(eraseWalletState = null) + is DetailsAction.ResetToFactory.Confirm -> + state.copy(cardSettingsState = state.cardSettingsState?.copy(resetConfirmed = action.confirmed)) else -> state } } @@ -90,68 +117,45 @@ private fun handleSecurityAction( action: DetailsAction.ManageSecurity, state: DetailsState, ): DetailsState { return when (action) { - is DetailsAction.ManageSecurity.SetCurrentOption -> { - val securityOption = when { - action.userCodes.isAccessCodeSet -> { - SecurityOption.AccessCode - } - action.userCodes.isPasscodeSet -> { - SecurityOption.PassCode - } - else -> { - SecurityOption.LongTap - } - } - state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption)) - } - is DetailsAction.ManageSecurity.OpenSecurity -> { - val allowedSecurityOptions = when { - state.scanResponse?.card?.isStart2Coin == true || - state.scanResponse?.isTangemNote() == true -> { - EnumSet.of(SecurityOption.LongTap) - } - state.scanResponse?.supportsBackup() == true -> { - EnumSet.of(state.securityScreenState?.currentOption) - } - else -> prepareAllowedSecurityOptions( - state.scanResponse?.card, state.securityScreenState?.currentOption - ) - } - state.copy(securityScreenState = state.securityScreenState?.copy( - allowedOptions = allowedSecurityOptions, - selectedOption = state.securityScreenState.currentOption - )) - } is DetailsAction.ManageSecurity.SelectOption -> { - state.copy(securityScreenState = state.securityScreenState?.copy( - selectedOption = action.option - )) - } - is DetailsAction.ManageSecurity.ConfirmSelection -> { - val confirmScreenState = when (action.option) { - SecurityOption.LongTap -> ConfirmScreenState.LongTap - SecurityOption.PassCode -> ConfirmScreenState.PassCode - SecurityOption.AccessCode -> ConfirmScreenState.AccessCode - } - state.copy(confirmScreenState = confirmScreenState) + val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy( + selectedOption = action.option, + ) + state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState)) } is DetailsAction.ManageSecurity.SaveChanges.Success -> { // Setting options to show only LongTap from now on for non-twins + val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy( + currentOption = state.cardSettingsState.manageSecurityState.selectedOption, + allowedOptions = state.scanResponse?.card?.let { + prepareAllowedSecurityOptions( + it, state.cardSettingsState.manageSecurityState.selectedOption, + ) + } ?: EnumSet.of(SecurityOption.LongTap), + ) state.copy( - securityScreenState = state.securityScreenState?.copy( - currentOption = state.securityScreenState.selectedOption, - allowedOptions = state.scanResponse?.card?.let { - prepareAllowedSecurityOptions( - it, state.securityScreenState.selectedOption - ) - } ?: EnumSet.of(SecurityOption.LongTap) - )) + cardSettingsState = state.cardSettingsState?.copy( + manageSecurityState = manageSecurityState, + ), + ) } - else -> state } } +private fun handlePrivacyAction( + action: DetailsAction.AppSettings, state: DetailsState, +): DetailsState { + return when (action) { + is DetailsAction.AppSettings.SwitchPrivacySetting -> { + when (action.setting) { + PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable) + PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable) + } + } + } +} + private fun prepareAllowedSecurityOptions( card: Card?, currentSecurityOption: SecurityOption?, ): EnumSet { @@ -169,7 +173,6 @@ private fun prepareAllowedSecurityOptions( return allowedSecurityOptions } - private fun Card.toCardInfo(): CardInfo { val cardId = this.cardId.chunked(4).joinToString(separator = " ") val issuer = this.issuer.name diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index ee6fd2bb7e..8e02856097 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux import android.net.Uri import com.tangem.blockchain.common.Wallet +import com.tangem.common.card.Card import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency @@ -14,13 +15,13 @@ import kotlin.properties.ReadOnlyProperty data class DetailsState( val scanResponse: ScanResponse? = null, val wallets: List = emptyList(), - val cardInfo: CardInfo? = null, - val eraseWalletState: EraseWalletState? = null, - val confirmScreenState: ConfirmScreenState? = null, - val securityScreenState: SecurityScreenState? = null, + val cardSettingsState: CardSettingsState? = null, val cardTermsOfUseUrl: Uri? = null, + val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, - val appCurrency: FiatCurrency = FiatCurrency.Default + val appCurrency: FiatCurrency = FiatCurrency.Default, + val saveWallets: Boolean = true, + val saveAccessCodes: Boolean = true, ) : StateType { // if you do not delegate - the application crashes on startup, @@ -39,13 +40,23 @@ data class CardInfo( val signedHashes: Int, ) -enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty } -enum class ConfirmScreenState { EraseWallet, LongTap, AccessCode, PassCode } -data class SecurityScreenState( +data class CardSettingsState( + val cardInfo: CardInfo, + val card: Card, + val manageSecurityState: ManageSecurityState?, + val resetCardAllowed: Boolean, + val resetConfirmed: Boolean = false, +) + +data class ManageSecurityState( val currentOption: SecurityOption = SecurityOption.LongTap, val selectedOption: SecurityOption = currentOption, val allowedOptions: EnumSet = EnumSet.allOf(SecurityOption::class.java), val buttonProceed: Button = Button(true), ) -enum class SecurityOption { LongTap, PassCode, AccessCode } \ No newline at end of file +enum class SecurityOption { LongTap, PassCode, AccessCode } + +enum class PrivacySetting { + SaveWallets, SaveAccessCode +} \ No newline at end of file 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 a180d20d4a..14a871fbd9 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 @@ -1,7 +1,6 @@ package com.tangem.tap.features.details.redux.walletconnect -import android.app.Activity -import com.tangem.domain.common.ScanResponse +import com.tangem.blockchain.common.Blockchain import com.tangem.tap.common.redux.NotificationAction import com.tangem.wallet.R import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder @@ -18,25 +17,21 @@ sealed class WalletConnectAction : Action { object RestoreSessions : WalletConnectAction() data class StartWalletConnect( - val activity: Activity, + val copiedUri: String?, ) : WalletConnectAction() data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() - object UnsupportedCard : WalletConnectAction() - data class OpenSession( val wcUri: String, ) : WalletConnectAction() - data class AddScanResponse( - val scanResponse: ScanResponse - ): WalletConnectAction() + data class SetNewSessionData( + val newSession: NewWcSessionData, + ) : WalletConnectAction() object RefuseOpeningSession : WalletConnectAction() - data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction() - data class ScanCard( val session: WalletConnectSession, val chainId: Int?, ) : WalletConnectAction() @@ -47,8 +42,18 @@ sealed class WalletConnectAction : Action { data class Success(val session: WalletConnectSession) : WalletConnectAction() } - data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction() + data class SwitchBlockchain( + val blockchain: Blockchain?, + val session: WalletConnectSession, + ) : WalletConnectAction() + data class SelectNetwork(val session: WalletConnectSession, val networks: List) : WalletConnectAction() + data class ChooseNetwork(val blockchain: Blockchain) : WalletConnectAction() + data class UpdateBlockchain( + val updatedSession: WalletConnectSession, + ) : WalletConnectAction() + + data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction() data class SetSessionsRestored(val sessions: List) : WalletConnectAction() 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 8dbaafc988..d98fb0b0f5 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 @@ -1,38 +1,35 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationParams -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.common.card.Card import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.getFromClipboard 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.NavigationAction import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.extensions.isMultiwalletAllowed -import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils +import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.wallet.redux.Currency -import com.tangem.tap.features.wallet.redux.WalletAction +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 +import timber.log.Timber class WalletConnectMiddleware { private val walletConnectManager = WalletConnectManager() - val walletConnectMiddleware: Middleware = { dispatch, state -> { next -> { action -> @@ -57,20 +54,36 @@ class WalletConnectMiddleware { } } is WalletConnectAction.StartWalletConnect -> { - val uri = action.activity.getFromClipboard()?.toString() + val uri = action.copiedUri if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) { store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan)) } } + is WalletConnectAction.SelectNetwork -> { + store.dispatch( + GlobalAction.ShowDialog( + WalletConnectDialog.ChooseNetwork( + session = action.session, + networks = action.networks, + ), + ), + ) + } + is WalletConnectAction.ChooseNetwork -> { + val data = state()?.walletConnectState?.newSessionData ?: return + scope.launch { + prepareWalletManager(data.scanResponse, store.state.walletState, action.blockchain, data.session) + } + } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { store.dispatchOnMain( GlobalAction.ShowDialog( WalletConnectDialog.ClipboardOrScanQr( - action.wcUri - ) - ) + action.wcUri, + ), + ), ) } is WalletConnectAction.OpeningSessionTimeout -> { @@ -92,8 +105,8 @@ class WalletConnectMiddleware { is WalletConnectAction.RefuseOpeningSession -> { store.dispatch( GlobalAction.ShowDialog( - WalletConnectDialog.OpeningSessionRejected - ) + WalletConnectDialog.OpeningSessionRejected, + ), ) } is WalletConnectAction.ScanCard -> { @@ -110,14 +123,14 @@ class WalletConnectMiddleware { transaction = action.transaction, session = action.session, id = action.id, - type = action.type + type = action.type, ) } is WalletConnectAction.HandlePersonalSignRequest -> { walletConnectManager.handlePersonalSignRequest( message = action.message, session = action.session, - id = action.id + id = action.id, ) } is WalletConnectAction.RejectRequest -> { @@ -138,9 +151,9 @@ class WalletConnectMiddleware { session = action.sessionData.session, sessionId = action.id, cardId = action.sessionData.wallet.cardId, - dAppName = action.sessionData.peerMeta.name - ) - ) + dAppName = action.sessionData.peerMeta.name, + ), + ), ) } is WalletConnectAction.BinanceTransaction.Transfer -> { @@ -152,126 +165,166 @@ class WalletConnectMiddleware { session = action.sessionData.session, sessionId = action.id, cardId = action.sessionData.wallet.cardId, - dAppName = action.sessionData.peerMeta.name - ) - ) + dAppName = action.sessionData.peerMeta.name, + ), + ), ) } is WalletConnectAction.BinanceTransaction.Sign -> { walletConnectManager.signBnb( - action.id, action.data, action.sessionData + action.id, action.data, action.sessionData, ) } + is WalletConnectAction.SwitchBlockchain -> { + if (action.session.wallet.derivationStyle == DerivationStyle.LEGACY) { + Timber.d("Cannot switch chains on AC01/AC02 wallets") + return + } + val blockchain = action.blockchain.guard { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork)) + return + } + val factory = WcWalletManagerFactory( + factory = store.state.globalState.tapWalletManager.walletManagerFactory, + currenciesRepository = currenciesRepository, + ) + val walletState = store.state.walletState + scope.launch { + val walletManager = factory.getWalletManager( + wallet = action.session.wallet, + blockchain = blockchain, + walletState = walletState, + ).guard { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.AddNetwork(blockchain.fullName), + ), + ) + return@launch + } + val updatedWallet = action.session.wallet.copy( + walletPublicKey = walletManager.wallet.publicKey.seedKey, + derivedPublicKey = walletManager.wallet.publicKey.derivedKey, + derivationPath = walletManager.wallet.publicKey.derivationPath, + blockchain = action.blockchain, + ) + val updatedSession = action.session.copy(wallet = updatedWallet) + store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) + } + } + is WalletConnectAction.UpdateBlockchain -> { + walletConnectManager.updateBlockchain(action.updatedSession) + } } } private fun scanCard(session: WalletConnectSession, chainId: Int?) { val blockchain = WalletConnectNetworkUtils.parseBlockchain( - chainId = chainId, peer = session.peerMeta + chainId = chainId, + peer = session.peerMeta, ) ?: Blockchain.Ethereum store.dispatch( GlobalAction.ScanCard( additionalBlockchainsToDerive = listOf(blockchain), onSuccess = { scanResponse -> - handleScanResponse(scanResponse, session, blockchain) + handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain) }, onFailure = { store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null)) - }, R.string.wallet_connect_scan_card_message - ) + }, + R.string.wallet_connect_scan_card_message, + ), ) } + private suspend fun getAvailableBlockchains(card: Card, walletState: WalletState): List { + return if (walletState.cardId == card.cardId) { + walletState.currencies.filter { + it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm() + }.map { it.blockchain } + } else { + currenciesRepository + .loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed) + .filter { + it.blockchain.isEvm() && + it.derivationPath == it.blockchain.derivationPath(card.derivationStyle)?.rawPath + } + .map { it.blockchain } + } + } + + private suspend fun prepareWalletManager( + scanResponse: ScanResponse, + walletState: WalletState, + blockchain: Blockchain, + session: WalletConnectSession, + ) { + val factory = WcWalletManagerFactory( + factory = store.state.globalState.tapWalletManager.walletManagerFactory, + currenciesRepository = currenciesRepository, + ) + val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard { + store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session)) + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.AddNetwork(blockchain.fullName), + ), + ) + return + } + val wallet = walletManager.wallet + val derivedKey = + if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) { + null + } else { + walletManager.wallet.publicKey.blockchainKey + } + val walletForSession = WalletForSession( + cardId = scanResponse.card.cardId, + walletPublicKey = wallet.publicKey.seedKey, + derivedPublicKey = derivedKey, + derivationPath = wallet.publicKey.derivationPath, + derivationStyle = scanResponse.card.derivationStyle, + blockchain = wallet.blockchain, + ) + + withMainContext { + val updatedSession = session.copy(wallet = walletForSession) + walletConnectManager.updateSession(updatedSession) + + store.dispatch(WalletConnectAction.ApproveSession(session.session)) + } + } + private fun handleScanResponse( - scanResponse: ScanResponse, session: WalletConnectSession, blockchain: Blockchain + scanResponse: ScanResponse, + session: WalletConnectSession, + blockchain: Blockchain, ) { val card = scanResponse.card if (!card.isMultiwalletAllowed) { store.dispatchOnMain(WalletConnectAction.UnsupportedCard) return } - - val walletManager = getWalletManager(scanResponse, blockchain).guard { - store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null)) - return - } - - val wallet = walletManager.wallet - val derivedKey = - if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) { - null - } else { - walletManager.wallet.publicKey.blockchainKey - } - val walletForSession = WalletForSession( - cardId = scanResponse.card.cardId, - walletPublicKey = wallet.publicKey.seedKey, - derivedPublicKey = derivedKey, - derivationPath = wallet.publicKey.derivationPath, - blockchain = wallet.blockchain + val walletState = store.state.walletState + val updatedSession = session.copy(wallet = session.wallet.copy(blockchain = blockchain)) + store.dispatch( + WalletConnectAction.SetNewSessionData( + NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain), + ), ) - val updatedSession = session.copy(wallet = walletForSession) - walletConnectManager.updateSession(updatedSession) - store.dispatchOnMain(WalletConnectAction.AddScanResponse(scanResponse)) + scope.launch { + val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList() - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.ApproveWcSession( - updatedSession + withMainContext { + store.dispatch( + GlobalAction.ShowDialog( + WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains), + ), ) - ) - ) - } - - private fun getWalletManager( - scanResponse: ScanResponse, blockchain: Blockchain - ): WalletManager? { - val card = scanResponse.card - val factory = store.state.globalState.tapWalletManager.walletManagerFactory - val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) { - Blockchain.EthereumTestnet - } else { - blockchain - } - - return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) { - val derivationPath = blockchainToMake.derivationPath(card.derivationStyle)?.rawPath - store.state.walletState.getWalletManager( - Currency.Blockchain(blockchainToMake, derivationPath) - ) - ?: factory.makeWalletManagerForApp( - scanResponse, - blockchainToMake, - card.derivationStyle?.let { DerivationParams.Default(it) } - ) - ?.also { walletManager -> - store.dispatch( - WalletAction.MultiWallet.AddBlockchain( - BlockchainNetwork.fromWalletManager(walletManager), walletManager - ) - ) - } - } else { - val walletManager = factory.makeWalletManagerForApp( - scanResponse, - blockchainToMake, - card.derivationStyle?.let { DerivationParams.Default(it) } - ) - scope.launch { - if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed) - .find { it.blockchain == blockchainToMake } != null - ) { - walletManager?.let { - currenciesRepository.saveUpdatedCurrency( - card.cardId, - BlockchainNetwork.fromWalletManager(walletManager) - ) - } - } } - return walletManager } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index 216367e9ee..544b6fa7be 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -13,18 +13,17 @@ class WalletConnectReducer { is WalletConnectAction.ApproveSession.Success -> { state.copy( loading = false, - sessions = state.sessions + action.session + sessions = state.sessions + action.session, ) } - is WalletConnectAction.OpenSession -> { + is WalletConnectAction.OpenSession -> { state.copy(loading = true) } - is WalletConnectAction.AddScanResponse -> { - state.copy(scanResponse = action.scanResponse) + is WalletConnectAction.SetNewSessionData -> { + state.copy(newSessionData = action.newSession) } is WalletConnectAction.SetSessionsRestored -> WalletConnectState(sessions = action.sessions) - is WalletConnectAction.RemoveSession -> { val sessions = state.sessions.filterNot { it.session.toUri() == action.session.toUri() } @@ -34,6 +33,11 @@ class WalletConnectReducer { is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false) is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false) is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false) + is WalletConnectAction.UpdateBlockchain -> state.copy( + sessions = state.sessions + .filterNot { it.peerId == action.updatedSession.peerId } + + action.updatedSession + ) else -> state } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 0039e247ee..0bdfaf69f6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.common.hdWallet.DerivationPath @@ -15,7 +16,13 @@ import com.trustwallet.walletconnect.models.session.WCSession data class WalletConnectState( val loading: Boolean = false, val sessions: List = listOf(), - val scanResponse: ScanResponse? = null + val newSessionData: NewWcSessionData? = null, +) + +data class NewWcSessionData( + val session: WalletConnectSession, + val scanResponse: ScanResponse, + val blockchain: Blockchain?, ) data class WalletConnectSession( @@ -37,8 +44,9 @@ data class WalletForSession( val walletPublicKey: ByteArray? = null, val derivedPublicKey: ByteArray? = null, val derivationPath: DerivationPath? = null, + val derivationStyle: DerivationStyle? = null, val isTestNet: Boolean = false, - val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum + val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum, ) { fun getBlockchainForSession(): Blockchain { @@ -82,18 +90,30 @@ data class WalletForSession( sealed class WalletConnectDialog : StateDialog { data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() object UnsupportedCard : WalletConnectDialog() + object UnsupportedNetwork : WalletConnectDialog() + data class AddNetwork(val network: String) : WalletConnectDialog() object OpeningSessionRejected : WalletConnectDialog() object SessionTimeout : WalletConnectDialog() - data class ApproveWcSession(val session: WalletConnectSession) : WalletConnectDialog() + data class ApproveWcSession( + val session: WalletConnectSession, + val networks: List, + ) : WalletConnectDialog() + + data class ChooseNetwork( + val session: WalletConnectSession, + val networks: List, + ) : WalletConnectDialog() + data class RequestTransaction(val dialogData: TransactionRequestDialogData) : WalletConnectDialog() data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog() - data class BnbTransactionDialog(val data: BinanceMessageData, - val session: WCSession, - val sessionId: Long, - val cardId: String, - val dAppName: String + data class BnbTransactionDialog( + val data: BinanceMessageData, + val session: WCSession, + val sessionId: Long, + val cardId: String, + val dAppName: String, ) : WalletConnectDialog() } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt deleted file mode 100644 index acbe36e4ae..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.tangem.tap.features.details.ui - -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.tap.common.extensions.getDrawable -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.details.redux.ConfirmScreenState -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentDetailsConfirmBinding -import org.rekotlin.StoreSubscriber - -class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm), - StoreSubscriber { - - private val binding: FragmentDetailsConfirmBinding by viewBinding(FragmentDetailsConfirmBinding::bind) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.toolbar.setNavigationOnClickListener { - store.dispatch(NavigationAction.PopBackTo()) - } - } - - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - setState(state) - } - - private fun setState(state: DetailsState) = with(binding) { - when (state.confirmScreenState) { - ConfirmScreenState.EraseWallet -> { - toolbar.title = getString(R.string.details_row_title_reset_factory_settings) - tvWarningDescription.text = getString(R.string.details_row_title_reset_factory_settings_warning) - btnConfirm.text = getString(R.string.details_row_title_reset_factory_settings) - btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getDrawable(R.drawable.ic_send), null - ) - btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) } - } - ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode, - ConfirmScreenState.PassCode -> { - toolbar.title = getString(R.string.details_manage_security_title) - tvWarningDescription.text = getString(R.string.details_security_management_warning) - btnConfirm.text = getString(R.string.common_save_changes) - btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getDrawable(R.drawable.ic_save), null - ) - btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt deleted file mode 100644 index e8f3b56a62..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.tangem.tap.features.details.ui - -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.domain.common.getTwinCardIdForUser -import com.tangem.tap.common.extensions.show -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.domain.extensions.isMultiwalletAllowed -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState -import com.tangem.tap.features.details.redux.SecurityOption -import com.tangem.tap.features.feedback.FeedbackEmail -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentDetailsBinding -import org.rekotlin.StoreSubscriber - -class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber { - - private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.toolbar.setNavigationOnClickListener { - store.dispatch(NavigationAction.PopBackTo()) - } - } - - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - setState(state) - } - - private fun setState(state: DetailsState) = with (binding){ - - if (state.cardInfo != null) { - val cardId = if (state.isTangemTwins) { - state.scanResponse?.card?.getTwinCardIdForUser() - } else { - state.cardInfo.cardId - } - tvCardId.text = cardId - tvIssuer.text = state.cardInfo.issuer - tvSignedHashes.text = getString( - R.string.details_row_subtitle_signed_hashes_format, - state.cardInfo.signedHashes.toString() - ) - } - - tvSignedHashes.show(!state.isTangemTwins) - tvSignedHashesTitle.show(!state.isTangemTwins) - - tvDisclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) } - - tvCardTou.show(state.cardTermsOfUseUrl != null) - tvCardTou.setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = state.cardTermsOfUseUrl - startActivity(intent) - } - - if (state.isTangemTwins) { - tvResetToFactory.show() - tvResetToFactory.text = getText(R.string.details_row_title_twins_recreate) - tvResetToFactory.setOnClickListener { - store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!)) - } - } else { - tvResetToFactory.show() - tvResetToFactory.text = getText(R.string.details_row_title_reset_factory_settings) - tvResetToFactory.setOnClickListener { - store.dispatch(DetailsAction.ResetToFactory.Check) - store.dispatch(DetailsAction.ResetToFactory.Proceed) - } - } - - tvCreateBackup.show(state.createBackupAllowed) - tvCreateBackup.setOnClickListener { - store.dispatch(DetailsAction.CreateBackup) - } - - tvAppCurrency.show(state.scanResponse?.card?.isMultiwalletAllowed != true) - tvAppCurrencyTitle.show(state.scanResponse?.card?.isMultiwalletAllowed != true) - tvAppCurrency.text = state.appCurrency.code - - tvAppCurrency.setOnClickListener { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } - - tvSendFeedback.setOnClickListener { - store.dispatch(GlobalAction.SendFeedback(FeedbackEmail())) - } - - tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true) - tvWalletConnect.setOnClickListener { - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) - } - - llManageSecurity.setOnClickListener { - store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card)) - } - - val currentSecurity = when (state.securityScreenState?.currentOption) { - SecurityOption.LongTap -> R.string.details_manage_security_long_tap - SecurityOption.PassCode -> R.string.details_manage_security_passcode - SecurityOption.AccessCode -> R.string.details_manage_security_access_code - null -> null - } - currentSecurity?.let { tvSecurity.text = getString(it) } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt deleted file mode 100644 index e9c8b38565..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.tap.features.details.ui - -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.common.card.Card -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState -import com.tangem.tap.features.details.redux.SecurityOption -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentDetailsSecurityBinding -import org.rekotlin.StoreSubscriber -import java.util.* - -class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), - StoreSubscriber { - - private val binding: FragmentDetailsSecurityBinding by viewBinding( - FragmentDetailsSecurityBinding::bind - ) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.toolbar.setNavigationOnClickListener { - store.dispatch(NavigationAction.PopBackTo()) - } - setOnClickListeners() - } - - private fun setOnClickListeners() { - binding.btnSaveChanges.setOnClickListener { - store.state.detailsState.securityScreenState?.selectedOption?.let { - store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection(it)) - } - } - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - - selectSecurityOption(state.securityScreenState?.selectedOption) - for (option in SecurityOption.values()) { - setupOption( - option, - state.securityScreenState?.allowedOptions - ?: EnumSet.noneOf(SecurityOption::class.java) - ) - } - binding.tvAccessCodeUnavailableDisclaimer.show( - state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup - ) - } - - private fun selectSecurityOption(securityOption: SecurityOption?) = with(binding) { - radiobuttonLongTap.isChecked = - securityOption == SecurityOption.LongTap && radiobuttonLongTap.isEnabled - radiobuttonPasscode.isChecked = - securityOption == SecurityOption.PassCode && radiobuttonPasscode.isEnabled - radiobuttonAccessCode.isChecked = - securityOption == SecurityOption.AccessCode && radiobuttonAccessCode.isEnabled - } - - private fun setupOption(option: SecurityOption, allowedOptions: EnumSet) { - when (option) { - SecurityOption.LongTap -> { enableLongTap(allowedOptions.contains(option)) } - SecurityOption.PassCode -> { enablePasscode(allowedOptions.contains(option)) } - SecurityOption.AccessCode -> { enableAccessCode(allowedOptions.contains(option)) } - } - } - - private fun enableLongTap(enable: Boolean) = with(binding) { - groupLongTap.show(enable) - val alpha = if (enable) 1f else 0.5f - tvLongTapDescription.alpha = alpha - tvLongTapTitle.alpha = alpha - radiobuttonLongTap.alpha = alpha - radiobuttonLongTap.isEnabled = enable - vLongTap.isClickable = enable - if (enable) { - vLongTap.setOnClickListener { - store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.LongTap)) - } - } - } - - private fun enablePasscode(enable: Boolean) = with(binding) { - groupPasscode.show(enable) - val alpha = if (enable) 1f else 0.5f - tvPasscodeDescription.alpha = alpha - tvPasscodeTitle.alpha = alpha - radiobuttonPasscode.alpha = alpha - radiobuttonPasscode.isEnabled = enable - vPasscode.isClickable = enable - if (enable) { - vPasscode.setOnClickListener { - store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.PassCode)) - } - } - } - - private fun enableAccessCode(enable: Boolean) = with(binding) { - groupAccessCode.show(enable) - val alpha = if (enable) 1f else 0.5f - tvAccessCodeDescription.alpha = alpha - tvAccessCodeTitle.alpha = alpha - radiobuttonAccessCode.alpha = alpha - radiobuttonAccessCode.isEnabled = enable - vAccessCode.isClickable = enable - if (enable) { - vAccessCode.setOnClickListener { - store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.AccessCode)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt new file mode 100644 index 0000000000..bea7b87171 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.features.details.ui.appsettings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.store +import com.tangem.wallet.R +import org.rekotlin.StoreSubscriber + +class AppSettingsFragment : Fragment(), StoreSubscriber { + private val viewModel = AppSettingsViewModel(store) + private var screenState: MutableState = + mutableStateOf(viewModel.updateState(store.state.detailsState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + AppSettingsScreen( + state = screenState.value, + onBackPressed = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + screenState.value = viewModel.updateState(state) + } +} diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt new file mode 100644 index 0000000000..34018707cf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -0,0 +1,230 @@ +package com.tangem.tap.features.details.ui.appsettings + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material.AlertDialog +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.details.redux.PrivacySetting +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.tap.features.details.ui.common.TangemSwitch +import com.tangem.wallet.R + +@Composable +fun AppSettingsScreen( + state: AppSettingsScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScreensScaffold( + content = { + AppSettings(state = state, modifier = modifier) + }, + titleRes = R.string.app_settings_title, + onBackClick = onBackPressed, + ) +} + +@Composable +private fun AppSettings( + state: AppSettingsScreenState, + modifier: Modifier = Modifier, +) { + var dialogType by remember { mutableStateOf(null) } + val onDialogStateChange: (PrivacySetting?) -> Unit = { dialogType = it } + + dialogType?.let { + SettingsAlertDialog( + element = it, + onDialogStateChange = onDialogStateChange, + onSettingToggled = state.onSettingToggled, + modifier = modifier, + ) + } + + Column( + modifier = modifier + .fillMaxSize(), + ) { + AppSettingsElement( + state = state, + setting = PrivacySetting.SaveWallets, + onDialogStateChange = onDialogStateChange, + modifier = modifier, + ) + Spacer(modifier = Modifier.size(32.dp)) + AppSettingsElement( + state = state, + setting = PrivacySetting.SaveAccessCode, + onDialogStateChange = onDialogStateChange, + modifier = modifier, + ) + } +} + +@Composable +private fun AppSettingsElement( + state: AppSettingsScreenState, + setting: PrivacySetting, + onDialogStateChange: (PrivacySetting?) -> Unit, + modifier: Modifier = Modifier, +) { + val titleRes = when (setting) { + PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet + PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes + } + val subtitleRes = when (setting) { + PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet_footer + PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer + } + val checked = state.settings[setting] ?: false + + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier + .weight(0.6f) + .padding(end = 4.dp), + ) { + Text( + text = stringResource(id = titleRes), + style = TangemTypography.subtitle1, + color = colorResource(id = R.color.text_primary_1), + ) + Spacer(modifier = Modifier.size(4.dp)) + Text( + text = stringResource(id = subtitleRes), + style = TangemTypography.body2, + color = colorResource(id = R.color.text_secondary), + ) + } + TangemSwitch( + checked = checked, + onCheckedChange = { + onCheckedChange( + element = setting, + enabled = it, + onSettingToggled = state.onSettingToggled, + onDialogStateChange = onDialogStateChange, + ) + }, + modifier = modifier + .padding(20.dp), + ) + } +} + +private fun onCheckedChange( + element: PrivacySetting, enabled: Boolean, + onSettingToggled: (PrivacySetting, Boolean) -> Unit, + onDialogStateChange: (PrivacySetting?) -> Unit, +) { + //Show warning if user wants to disable the switch + if (!enabled) { + onDialogStateChange(element) + } else { + onSettingToggled(element, enabled) + } +} + +@Composable +private fun SettingsAlertDialog( + element: PrivacySetting, + onDialogStateChange: (PrivacySetting?) -> Unit, + onSettingToggled: (PrivacySetting, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val text = when (element) { + PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message + PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message + } + AlertDialog( + onDismissRequest = { onDialogStateChange(null) }, + confirmButton = { + TextButton( + onClick = { + onDialogStateChange(null) + }, + ) { + Text( + text = stringResource(id = R.string.common_cancel), + color = colorResource(id = R.color.text_secondary), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + } + }, + dismissButton = { + TextButton( + onClick = { + onDialogStateChange(null) + onSettingToggled(element, false) + }, + modifier = modifier.padding(bottom = 14.dp), + ) { + Text( + text = stringResource(id = R.string.common_delete), + color = colorResource(id = R.color.text_warning), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + } + }, + title = { + Text( + text = stringResource(id = R.string.common_attention), + color = colorResource(id = R.color.text_primary_1), + ) + }, + text = { + Text( + text = stringResource(id = text), + color = colorResource(id = R.color.text_secondary), + ) + }, + shape = MaterialTheme.shapes.medium.copy(all = CornerSize(size = 28.dp)), + modifier = modifier.padding(vertical = 34.dp, horizontal = 24.dp), + ) +} + +@Preview +@Composable +fun AppSettingsScreenPreview() { + AppSettingsScreen( + state = AppSettingsScreenState( + settings = mapOf( + PrivacySetting.SaveWallets to true, + PrivacySetting.SaveAccessCode to false, + ), + onSettingToggled = { _, _ -> }, + ), + onBackPressed = { }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt new file mode 100644 index 0000000000..043423b3d3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.tap.features.details.redux.PrivacySetting + +data class AppSettingsScreenState( + val settings: Map, + val onSettingToggled: (PrivacySetting, Boolean) -> Unit, +) + + diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt new file mode 100644 index 0000000000..65dd12eaeb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.details.redux.PrivacySetting +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.rekotlin.Store + +class AppSettingsViewModel(private val store: Store) { + private val _uiState = MutableStateFlow(updateState(store.state.detailsState)) + val uiState: StateFlow = _uiState + fun updateState(state: DetailsState): AppSettingsScreenState { + return AppSettingsScreenState( + settings = mapOf( + PrivacySetting.SaveWallets to state.saveWallets, + PrivacySetting.SaveAccessCode to state.saveAccessCodes, + ), + onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) }, + ) + } + + private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) { + store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt new file mode 100644 index 0000000000..a679020e84 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -0,0 +1,73 @@ +package com.tangem.tap.features.details.ui.cardsettings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class CardSettingsFragment : Fragment(), StoreSubscriber { + + private val viewModel = CardSettingsViewModel(store) + + private var screenState: MutableState = + mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(android.R.transition.fade) + exitTransition = inflater.inflateTransition(android.R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + CardSettingsScreen( + state = screenState.value, + onBackPressed = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + screenState.value = viewModel.updateState(state.cardSettingsState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt new file mode 100644 index 0000000000..92eec4c59b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -0,0 +1,169 @@ +package com.tangem.tap.features.details.ui.cardsettings + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.details.ui.common.DetailsMainButton +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.wallet.R + +@Composable +fun CardSettingsScreen( + state: CardSettingsScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScreensScaffold( + content = + if (state.cardDetails == null) { + { CardSettingsReadCard(state.onScanCardClick, modifier = modifier) } + } else { + { CardSettings(state = state, modifier = modifier) } + }, + titleRes = R.string.card_settings_title, + onBackClick = onBackPressed, + ) +} + +@Composable +fun CardSettingsReadCard( + onScanCardClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + + Box( + modifier = modifier + .fillMaxWidth(), + + ) { + Image( + modifier = modifier + .fillMaxWidth() + .padding(start = 80.dp, end = 80.dp, top = 70.dp) + .rotate(-15f), + painter = painterResource(id = R.drawable.card_placeholder_secondary), + contentDescription = "", + contentScale = ContentScale.FillWidth, + ) + Image( + modifier = modifier + .fillMaxWidth() + .padding(start = 60.dp, end = 60.dp) + .rotate(-1f), + painter = painterResource(id = R.drawable.card_placeholder_black), + contentDescription = "", + contentScale = ContentScale.FillWidth, + ) + + } + + + Column( + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 32.dp), + ) { + + Text( + text = stringResource(id = R.string.scan_card_settings_title), + color = colorResource(id = R.color.text_primary_1), + style = TangemTypography.headline3, + ) + Spacer(modifier = modifier.size(20.dp)) + Text( + text = stringResource(id = R.string.scan_card_settings_message), + color = colorResource(id = R.color.text_secondary), + style = TangemTypography.body1, + ) + Spacer(modifier = modifier.size(29.dp)) + DetailsMainButton( + title = stringResource(id = R.string.scan_card_settings_button), + onClick = onScanCardClick, + modifier = modifier, + ) + } + } +} + +@Composable +fun CardSettings( + state: CardSettingsScreenState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + ) { + state.cardDetails?.map { + val paddingBottom = when (it) { + is CardInfo.CardId, is CardInfo.Issuer -> 12.dp + is CardInfo.SignedHashes -> 14.dp + is CardInfo.SecurityMode -> 16.dp + is CardInfo.ChangeAccessCode -> 16.dp + is CardInfo.ResetToFactorySettings -> 28.dp + } + val paddingTop = when (it) { + is CardInfo.CardId -> 0.dp + is CardInfo.Issuer -> 12.dp + is CardInfo.SignedHashes -> 12.dp + is CardInfo.SecurityMode -> 14.dp + is CardInfo.ChangeAccessCode -> 16.dp + is CardInfo.ResetToFactorySettings -> 16.dp + } + Column( + modifier = modifier + .fillMaxWidth() + .clickable( + enabled = it.clickable, + onClick = { state.onElementClick(it) }, + ) + .padding(start = 20.dp, end = 20.dp, bottom = paddingBottom, top = paddingTop), + ) { + val titleColor = if (it.clickable) R.color.text_primary_1 else R.color.text_tertiary + val subtitleColor = if (it.clickable) R.color.text_secondary else R.color.text_tertiary + Text( + text = it.titleRes.resolveReference(), + color = colorResource(id = titleColor), + style = TangemTypography.subtitle1, + ) + Spacer(modifier = modifier.size(4.dp)) + Text( + text = it.subtitle.resolveReference(), + color = colorResource(id = subtitleColor), + style = TangemTypography.body2, + ) + } + + } + + } +} + +@Composable +@Preview +fun CardSettingsPreview() { + CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt new file mode 100644 index 0000000000..e7354df5c5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.features.details.ui.cardsettings + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource +import com.tangem.tap.features.details.redux.SecurityOption +import com.tangem.tap.features.details.ui.securitymode.toTitleRes +import com.tangem.wallet.R + +data class CardSettingsScreenState( + val cardDetails: List? = null, + val onScanCardClick: () -> Unit, + val onElementClick: (CardInfo) -> Unit, +) + +sealed class CardInfo( + val titleRes: TextReference, val subtitle: TextReference, val clickable: Boolean = false, +) { + class CardId(subtitle: String) : CardInfo( + titleRes = TextReference.Res(R.string.details_row_title_cid), + subtitle = TextReference + .Str(subtitle), + ) + + class Issuer(subtitle: String) : CardInfo( + titleRes = TextReference.Res(R.string.details_row_title_issuer), + subtitle = TextReference + .Str(subtitle), + ) + + class SignedHashes(hashes: String) : CardInfo( + titleRes = TextReference.Res(R.string.details_row_title_signed_hashes), + subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, hashes), + ) + + class SecurityMode( + securityOption: SecurityOption, + clickable: Boolean, + ) : CardInfo( + titleRes = TextReference.Res(R.string.card_settings_security_mode), + subtitle = TextReference.Res(securityOption.toTitleRes()), + clickable = clickable, + ) + + object ChangeAccessCode : CardInfo( + titleRes = TextReference.Res(R.string.card_settings_change_access_code), + subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer), + clickable = true, + ) + + object ResetToFactorySettings : CardInfo( + titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory), + subtitle = TextReference.Res(R.string.card_settings_reset_card_to_factory_footer), + clickable = true, + ) +} + +sealed interface TextReference { + class Res(@StringRes val id: Int, val formatArgs: List = emptyList()) : TextReference { + constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) + } + + class Str(val value: String) : TextReference +} + +@Composable +@ReadOnlyComposable +fun TextReference.resolveReference(): String { + return when (this) { + is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) + is TextReference.Str -> this.value + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..60affefd8b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -0,0 +1,73 @@ +package com.tangem.tap.features.details.ui.cardsettings + +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 +import org.rekotlin.Store + +class CardSettingsViewModel(private val store: Store) { + + fun updateState(state: CardSettingsState?): CardSettingsScreenState { + + return if (state?.manageSecurityState == null) { + CardSettingsScreenState( + cardDetails = null, + onElementClick = {}, + onScanCardClick = { + store.dispatch(DetailsAction.ScanCard) + }, + ) + } else { + val cardId = if (state.card.isTangemTwins()) { + state.card.getTwinCardIdForUser() + } else { + state.cardInfo.cardId + } + val cardDetails: MutableList = mutableListOf( + CardInfo.CardId(cardId), + CardInfo.Issuer(state.cardInfo.issuer), + ) + + if (!state.card.isTangemTwins()) { + cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString())) + } + cardDetails.add( + CardInfo.SecurityMode( + securityOption = state.manageSecurityState.currentOption, + clickable = state.manageSecurityState.allowedOptions.size > 1, + ), + ) + if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) { + cardDetails.add(CardInfo.ChangeAccessCode) + } + if (state.resetCardAllowed) { + cardDetails.add(CardInfo.ResetToFactorySettings) + } + + CardSettingsScreenState( + cardDetails = cardDetails, + onScanCardClick = { }, + onElementClick = { + handleClickingItem(it) + }, + ) + } + } + + private fun handleClickingItem(item: CardInfo) { + when (item) { + is CardInfo.ChangeAccessCode -> { + store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode) + } + is CardInfo.ResetToFactorySettings -> { + store.dispatch(DetailsAction.ResetToFactory.Start) + } + is CardInfo.SecurityMode -> { + store.dispatch(DetailsAction.ManageSecurity.OpenSecurity) + } + else -> {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt new file mode 100644 index 0000000000..d8e6bf1b11 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.features.details.ui.common + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.wallet.R + +@Composable +fun SettingsScreensScaffold( + content: @Composable () -> Unit, + background: @Composable (() -> Unit)? = null, + fab: @Composable (() -> Unit)? = null, + backgroundColor: Color = colorResource(id = R.color.background_primary), + titleRes: Int? = null, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(true, onBackClick) + + Scaffold( + topBar = { + EmptyTopBarWithNavigation( + onBackClick = onBackClick, + backgroundColor = backgroundColor, + ) + }, + modifier = modifier.systemBarsPadding(), + backgroundColor = backgroundColor, + floatingActionButton = { fab?.invoke() }, + ) { + if (titleRes != null) { + Box(modifier = modifier.fillMaxSize()) { + background?.invoke() + + Column(modifier = modifier.fillMaxWidth()) { + Text( + text = stringResource(id = titleRes), + modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp), + style = TangemTypography.headline1, + color = colorResource(id = R.color.text_primary_1), + ) + content() + } + } + } else { + content() + } + } +} + +@Composable +fun ScreenTitle( + titleRes: Int, + modifier: Modifier = Modifier, +) { + Text( + text = stringResource(id = titleRes), + modifier = modifier.padding(start = 20.dp, end = 20.dp), + style = TangemTypography.headline1, + color = colorResource(id = R.color.text_primary_1), + ) +} + +@Composable +fun EmptyTopBarWithNavigation( + onBackClick: () -> Unit, + backgroundColor: Color = colorResource(id = R.color.background_primary), + modifier: Modifier = Modifier, +) { + TopAppBar( + title = { }, + navigationIcon = + { + IconButton(onClick = onBackClick) { + Icon( + painterResource(id = R.drawable.ic_back), "", + tint = colorResource(id = R.color.icon_primary_1), + ) + } + }, + backgroundColor = backgroundColor, + elevation = 0.dp, + ) +} + +@Composable +fun DetailsMainButton( + title: String, + enabled: Boolean = true, + onClick: (() -> Unit), + modifier: Modifier = Modifier, +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + enabled = enabled, + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(R.color.button_primary), + contentColor = colorResource(R.color.text_primary_2), + disabledBackgroundColor = colorResource(R.color.button_disabled), + disabledContentColor = colorResource(R.color.text_disabled), + ), + ) { + Text(text = title) + Spacer(modifier = modifier.size(8.dp)) + Icon(painter = painterResource(id = R.drawable.ic_tangem), contentDescription = "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt new file mode 100644 index 0000000000..bf5268bf6f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt @@ -0,0 +1,96 @@ +package com.tangem.tap.features.details.ui.common + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.wallet.R + +@Composable +fun TangemSwitch( + modifier: Modifier = Modifier, + enabledColor: Color = colorResource(id = R.color.control_checked), + disabledColor: Color = colorResource(id = R.color.icon_informative), + size: Dp = 48.dp, + checked: Boolean = false, + onCheckedChange: (Boolean) -> Unit, +) { + val transition = updateTransition(checked, label = "SwitchState") + val color by transition.animateColor( + transitionSpec = { + tween(durationMillis = 200, easing = FastOutLinearInEasing) + }, + label = "", + ) { enabled -> + if (enabled) enabledColor else disabledColor + } + val interactionSource = remember { MutableInteractionSource() } + + Box( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + ) { + onCheckedChange(!checked) + } + .indication( + interactionSource = interactionSource, + indication = rememberRipple( + bounded = false, + color = Color.Transparent, + ), + ), + ) { + BoxWithConstraints( + modifier = modifier + .width(size) + .height(size / 2) + .indication(interactionSource, null) + .background(color = color, shape = RoundedCornerShape(100)), + contentAlignment = Alignment.CenterStart, + ) { + val roundCardSize = this.maxWidth / 2 + val xOffset by transition.animateDp( + transitionSpec = { + tween(durationMillis = 150, easing = LinearOutSlowInEasing) + }, + label = "xOffset", + ) { enabled -> + if (enabled) this.maxWidth - roundCardSize else 0.dp + } + + Box( + modifier = Modifier + .size(this.maxWidth / 2) + .offset(x = xOffset, y = 0.dp) + .padding(3.dp) + .background(color = Color.White, shape = RoundedCornerShape(100)), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt new file mode 100644 index 0000000000..6674371319 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.features.details.ui.details + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class DetailsFragment : Fragment(), StoreSubscriber { + + private val detailsViewModel = DetailsViewModel(store) + + private var detailsScreenState: MutableState = + mutableStateOf(detailsViewModel.updateState(store.state.detailsState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(android.R.transition.fade) + exitTransition = inflater.inflateTransition(android.R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + DetailsScreen( + state = detailsScreenState.value, + onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + detailsScreenState.value = detailsViewModel.updateState(state) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..24129ec514 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -0,0 +1,197 @@ +package com.tangem.tap.features.details.ui.details + +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.details.ui.common.ScreenTitle +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.wallet.R + +@Composable +fun DetailsScreen( + state: DetailsScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScreensScaffold( + content = { Content(state = state, modifier = modifier) }, + onBackClick = onBackPressed, + ) +} + +@Composable +fun Content( + state: DetailsScreenState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + ScreenTitle(titleRes = R.string.details_title, modifier.padding(bottom = 52.dp)) + state.elements.map { + if (it == SettingsElement.WalletConnect) { + WalletConnectDetailsItem( + onItemsClick = state.onItemsClick, + modifier = modifier, + ) + } else { + DetailsItem( + item = it, + appCurrency = state.appCurrency, + onItemsClick = state.onItemsClick, + modifier = modifier, + ) + } + } + Spacer(modifier = modifier.weight(1f)) + TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", + style = TangemTypography.caption, + color = colorResource(id = R.color.text_tertiary), + modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + ) + } +} + +@Composable +fun WalletConnectDetailsItem( + onItemsClick: (SettingsElement) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(84.dp) + .fillMaxWidth() + .clickable { onItemsClick(SettingsElement.WalletConnect) }, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_walletconnect), + contentDescription = stringResource(id = R.string.wallet_connect_title), + modifier = modifier.padding(start = 20.dp, end = 20.dp), + tint = colorResource(id = R.color.all_colors_azure), + ) + Column( + modifier = modifier.height(56.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.wallet_connect_title), + modifier = modifier.padding(end = 20.dp, bottom = 4.dp), + style = TangemTypography.headline3, + color = colorResource(id = R.color.text_primary_1), + ) + Text( + text = stringResource(id = R.string.wallet_connect_subtitle), + modifier = modifier.padding(end = 20.dp, bottom = 4.dp), + style = TangemTypography.body1, + color = colorResource(id = R.color.text_secondary), + ) + } + } +} + +@Composable +fun DetailsItem( + item: SettingsElement, + appCurrency: String, + onItemsClick: (SettingsElement) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(56.dp) + .fillMaxWidth() + .clickable { onItemsClick(item) }, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = item.iconRes), + contentDescription = stringResource(id = item.titleRes), + modifier = modifier.padding(start = 20.dp, end = 20.dp), + tint = colorResource(id = R.color.icon_secondary), + ) + Column(modifier = modifier.padding(end = 20.dp)) { + Text( + text = stringResource(id = item.titleRes), + modifier = modifier, + style = TangemTypography.subtitle1, + color = colorResource(id = R.color.text_primary_1), + ) + if (item == SettingsElement.AppCurrency) { + Text( + text = appCurrency, + modifier = modifier, + style = TangemTypography.body2, + color = colorResource(id = R.color.text_secondary), + ) + } + } + } +} + +@Composable +fun TangemSocialAccounts( + links: List, + onSocialNetworkClick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier.padding(start = 8.dp, end = 8.dp), + ) { + items(links) { + Icon( + painter = painterResource(id = it.iconRes), + contentDescription = "", + modifier = modifier + .padding(8.dp) + .clickable { onSocialNetworkClick(it.url) }, + tint = colorResource(id = R.color.icon_informative), + ) + } + } +} + +@Composable +@Preview +fun Preview() { + DetailsScreen( + state = DetailsScreenState( + elements = SettingsElement.values().toList(), + tangemLinks = TangemSocialAccounts.accountsEn, + tangemVersion = "Tangem 2.14.12 (343)", + appCurrency = "Dollar", + onItemsClick = {}, onSocialNetworkClick = {}, + ), + onBackPressed = {}, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt new file mode 100644 index 0000000000..1962a53261 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.features.details.ui.details + +import com.tangem.wallet.R + +data class DetailsScreenState( + val elements: List, + val tangemLinks: List, + val tangemVersion: String, + val appCurrency: String, + val onItemsClick: (SettingsElement) -> Unit, + val onSocialNetworkClick: (String) -> Unit, +) { + val appNameRes: Int = R.string.app_name +} + +enum class SettingsElement( + val iconRes: Int, + val titleRes: Int, +) { + WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), + Chat(R.drawable.ic_chat, R.string.details_ask_a_question), + SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), + CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), + AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency), + AppSettings(R.drawable.ic_settings, R.string.app_settings_title), + LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), + TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App + TermsOfUse(R.drawable.ic_text, R.string.details_row_title_card_tou), // Terms of Use for S2C cards only + PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy), + ; +} + +data class TangemLink( + val iconRes: Int, + val url: String, +) + +object TangemSocialAccounts { + val accountsEn: List = listOf( + TangemLink(R.drawable.ic_telegram, "https://t.me/TangemCards"), + TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"), + TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"), + TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"), + TangemLink(R.drawable.ic_github, "https://github.com/tangem"), + TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"), + ) + val accountsRu: List = listOf( + TangemLink(R.drawable.ic_telegram, "https://t.me/tangem_ru"), + TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"), + TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"), + TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"), + TangemLink(R.drawable.ic_github, "https://github.com/tangem"), + TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"), + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..97ce019a84 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -0,0 +1,104 @@ +package com.tangem.tap.features.details.ui.details + +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.tap.common.feedback.FeedbackEmail +import com.tangem.tap.common.feedback.SupportInfo +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.NavigationAction +import com.tangem.tap.domain.extensions.isMultiwalletAllowed +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.home.LocaleRegionProvider +import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.wallet.BuildConfig +import org.rekotlin.Store + +class DetailsViewModel(private val store: Store) { + fun updateState(state: DetailsState): DetailsScreenState { + val settings = SettingsElement.values().mapNotNull { + when (it) { + SettingsElement.WalletConnect -> { + if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null + } + SettingsElement.LinkMoreCards -> { + if (state.createBackupAllowed) it else null + } + SettingsElement.PrivacyPolicy -> { + if (state.privacyPolicyUrl != null) it else null + } + SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen + SettingsElement.AppCurrency -> if (state.scanResponse?.card?.isMultiwalletAllowed != true) it else null + SettingsElement.TermsOfUse -> if (state.scanResponse?.card?.isStart2Coin == true) it else null + else -> it + } + } + + return DetailsScreenState( + settings, + tangemLinks = getSocialLinks(), + tangemVersion = getTangemAppVersion(), + appCurrency = state.appCurrency.name, + onItemsClick = { handleClickingSettingsItem(it) }, + onSocialNetworkClick = { handleSocialNetworkClick(it) }, + ) + } + + private fun handleSocialNetworkClick(url: String) { + store.dispatch(NavigationAction.OpenUrl(url)) + } + + private fun handleClickingSettingsItem(item: SettingsElement) { + when (item) { + SettingsElement.WalletConnect -> { + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + } + SettingsElement.Chat -> { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + SettingsElement.SendFeedback -> { + store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } + SettingsElement.CardSettings -> { + store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) + } + SettingsElement.AppCurrency -> { + store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) + } + SettingsElement.AppSettings -> { + store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) //TODO: To be available later + } + SettingsElement.LinkMoreCards -> { + store.dispatch(DetailsAction.CreateBackup) + } + SettingsElement.TermsOfService -> { + store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + } + SettingsElement.TermsOfUse -> { + store.dispatch(DetailsAction.ShowDisclaimer) + } + SettingsElement.PrivacyPolicy -> { + // TODO: To be available later + } + } + } + + private fun getSocialLinks(): List { + val locale = LocaleRegionProvider().getRegion() + return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { + TangemSocialAccounts.accountsRu + } else { + TangemSocialAccounts.accountsEn + } + } + + private fun getTangemAppVersion(): String { + val versionCode: Int = BuildConfig.VERSION_CODE + val versionName: String = BuildConfig.VERSION_NAME + return "$versionName ($versionCode)" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt new file mode 100644 index 0000000000..fa63874859 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.features.details.ui.resetcard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class ResetCardFragment : Fragment(), StoreSubscriber { + + private val viewModel = ResetCardViewModel(store) + + private var screenState: MutableState = + mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(android.R.transition.fade) + exitTransition = inflater.inflateTransition(android.R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + ResetCardScreen( + state = screenState.value, + onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + screenState.value = viewModel.updateState(state.cardSettingsState) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..2c07b092ad --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -0,0 +1,144 @@ +package com.tangem.tap.features.details.ui.resetcard + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconToggleButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.details.ui.common.DetailsMainButton +import com.tangem.tap.features.details.ui.common.ScreenTitle +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.wallet.R + +@Composable +fun ResetCardScreen( + state: ResetCardScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.background(colorResource(id = R.color.background_primary))) { + Image( + painter = painterResource(id = R.drawable.ic_reset_background), + contentDescription = "", + modifier = modifier.offset(y = (-16).dp), + ) + SettingsScreensScaffold( + content = { ResetCardView(state = state, modifier = modifier) }, + onBackClick = onBackPressed, + backgroundColor = Color.Transparent, + ) + } +} + +@Composable +fun ResetCardView( + state: ResetCardScreenState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Bottom, + ) { + Box( + modifier = modifier, + ) { + ScreenTitle(titleRes = R.string.reset_card_to_factory_navigation_title) + } + Spacer( + modifier = modifier + .defaultMinSize(20.dp) + .weight(1f), + ) + Text( + text = stringResource(id = R.string.common_attention), + modifier = modifier.padding(start = 20.dp, end = 20.dp), + style = TangemTypography.headline3, + color = colorResource(id = R.color.text_primary_1), + ) + + Spacer(modifier = modifier.size(24.dp)) + + Text( + text = stringResource(id = R.string.reset_card_to_factory_message), + modifier = modifier.padding(start = 20.dp, end = 20.dp), + style = TangemTypography.body1, + color = colorResource(id = R.color.text_secondary), + ) + + Spacer(modifier = modifier.size(44.dp)) + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 20.dp), + ) { + IconToggleButton( + checked = state.accepted, + onCheckedChange = state.onAcceptWarningToggleClick, + modifier = modifier.padding(start = 20.dp, end = 20.dp), + ) { + Icon( + painter = painterResource( + if (state.accepted) { + R.drawable.ic_accepted + } else { + R.drawable.ic_unticked + }, + ), + contentDescription = null, + tint = if (state.accepted) { + colorResource(id = R.color.icon_accent) + } else { + colorResource(id = R.color.icon_secondary) + }, + ) + } + Text( + text = stringResource(id = R.string.reset_card_to_factory_warning_message), + style = TangemTypography.body2, + color = colorResource(id = R.color.text_secondary), + ) + } + + Spacer(modifier = modifier.size(32.dp)) + Box( + modifier = modifier + .padding(start = 16.dp, end = 16.dp, bottom = 32.dp), + ) { + DetailsMainButton( + title = stringResource(id = R.string.reset_card_to_factory_button_title), + onClick = state.onResetButtonClick, + enabled = state.resetButtonEnabled, + ) + } + + } +} + +@Composable +@Preview +fun ResetCardScreenPreview( + +) { + ResetCardScreen(state = ResetCardScreenState(onAcceptWarningToggleClick = {}, accepted = true) {}, {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt new file mode 100644 index 0000000000..f658eac463 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.details.ui.resetcard + +data class ResetCardScreenState( + val accepted: Boolean = false, + val onAcceptWarningToggleClick: (Boolean) -> Unit, + val onResetButtonClick: () -> Unit, +) { + val resetButtonEnabled: Boolean + get() = accepted +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt new file mode 100644 index 0000000000..15f891dba3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.features.details.ui.resetcard + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.redux.CardSettingsState +import com.tangem.tap.features.details.redux.DetailsAction +import org.rekotlin.Store + +class ResetCardViewModel(private val store: Store) { + + fun updateState(state: CardSettingsState?): ResetCardScreenState { + return ResetCardScreenState( + accepted = state?.resetConfirmed ?: false, + onAcceptWarningToggleClick = { store.dispatch(DetailsAction.ResetToFactory.Confirm(it)) }, + onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt new file mode 100644 index 0000000000..96d55a808a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.features.details.ui.securitymode + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class SecurityModeFragment : Fragment(), StoreSubscriber { + + private val viewModel = SecurityModeViewModel(store) + + private var screenState: MutableState = + mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(android.R.transition.fade) + exitTransition = inflater.inflateTransition(android.R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + SecurityModeScreen( + state = screenState.value, + onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt new file mode 100644 index 0000000000..e18146767f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -0,0 +1,132 @@ +package com.tangem.tap.features.details.ui.securitymode + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.details.redux.SecurityOption +import com.tangem.tap.features.details.ui.common.DetailsMainButton +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.wallet.R + +@Composable +fun SecurityModeScreen( + state: SecurityModeScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScreensScaffold( + content = { SecurityModeOptions(state = state, modifier = modifier) }, + titleRes = R.string.card_settings_security_mode, + onBackClick = onBackPressed, + ) +} + +@Composable +fun SecurityModeOptions( + state: SecurityModeScreenState, + modifier: Modifier = Modifier, +) { + + Column( + modifier = modifier + .fillMaxSize() + .padding(bottom = 28.dp) + .offset(y = (-16).dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + state.availableOptions.map { + SecurityOption(option = it, state = state, modifier = modifier) + } + + Spacer(modifier = Modifier.weight(1f)) + + DetailsMainButton( + title = stringResource(id = R.string.common_save_changes), + enabled = state.isSaveChangesEnabled, + onClick = state.onSaveChangesClicked, + modifier = modifier + .padding(start = 20.dp, end = 20.dp), + ) + } +} + +@Composable +fun SecurityOption( + option: SecurityOption, state: SecurityModeScreenState, + modifier: Modifier, +) { + val selected = option == state.selectedSecurityMode + + val title = option.toTitleRes() + + val subtitle = when (option) { + SecurityOption.LongTap -> R.string.details_manage_security_long_tap_description + SecurityOption.PassCode -> R.string.details_manage_security_passcode_description + SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description + } + + Row( + modifier = modifier + .fillMaxWidth() + .selectable( + selected = selected, onClick = { state.onNewModeSelected(option) }, + ) + .padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp), + ) { + + RadioButton( + selected = selected, onClick = null, + modifier = modifier.padding(end = 20.dp), + colors = RadioButtonDefaults.colors( + unselectedColor = colorResource(id = R.color.icon_secondary), + selectedColor = colorResource(id = R.color.icon_accent), + ), + ) + + Column { + Text( + text = stringResource(id = title), + style = TangemTypography.subtitle1, + color = colorResource(id = R.color.text_primary_1), + ) + Spacer(modifier = modifier.size(4.dp)) + Text( + text = stringResource(id = subtitle), + style = TangemTypography.body2, + color = colorResource(id = R.color.text_secondary), + ) + } + } +} + +@Preview +@Composable +fun SecurityModeScreenPreview() { + SecurityModeScreen( + state = SecurityModeScreenState( + availableOptions = SecurityOption.values().toList(), + selectedSecurityMode = SecurityOption.LongTap, + isSaveChangesEnabled = false, + onNewModeSelected = {}, + onSaveChangesClicked = {}, + ), + onBackPressed = {}, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt new file mode 100644 index 0000000000..97482c7c01 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.details.ui.securitymode + +import com.tangem.tap.features.details.redux.SecurityOption +import com.tangem.wallet.R + +data class SecurityModeScreenState( + val availableOptions: List, + val selectedSecurityMode: SecurityOption, + val isSaveChangesEnabled: Boolean, + val onNewModeSelected: (SecurityOption) -> Unit, + val onSaveChangesClicked: () -> Unit, +) + +fun SecurityOption.toTitleRes(): Int { + return when (this) { + SecurityOption.LongTap -> R.string.details_manage_security_long_tap + SecurityOption.PassCode -> R.string.details_manage_security_passcode + SecurityOption.AccessCode -> R.string.details_manage_security_access_code + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt new file mode 100644 index 0000000000..b25277864d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.details.ui.securitymode + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.ManageSecurityState +import com.tangem.tap.features.details.redux.SecurityOption +import org.rekotlin.Store + +class SecurityModeViewModel(val store: Store) { + + fun updateState(state: ManageSecurityState?): SecurityModeScreenState { + if (state == null) return SecurityModeScreenState( + availableOptions = emptyList(), + selectedSecurityMode = SecurityOption.LongTap, + isSaveChangesEnabled = false, + onNewModeSelected = {}, + onSaveChangesClicked = {}, + ) + return SecurityModeScreenState( + availableOptions = state.allowedOptions.toList(), + selectedSecurityMode = state.selectedOption, + isSaveChangesEnabled = state.selectedOption != state.currentOption, + onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) }, + onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt new file mode 100644 index 0000000000..8d3de292ae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -0,0 +1,76 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.store +import org.rekotlin.StoreSubscriber + +class WalletConnectFragment : Fragment(), StoreSubscriber { + private val viewModel = WalletConnectViewModel(store) + private var screenState: MutableState = + mutableStateOf(viewModel.updateState(store.state.walletConnectState)) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(android.R.transition.fade) + exitTransition = inflater.inflateTransition(android.R.transition.fade) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + isTransitionGroup = true + AppCompatTheme { + WalletConnectScreen( + state = screenState.value, + onBackPressed = { + if (screenState.value.isLoading) { + store.dispatch( + WalletConnectAction.FailureEstablishingSession( + store.state.walletConnectState.newSessionData?.session?.session, + ), + ) + } + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + } + } + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.walletConnectState == newState.walletConnectState + }.select { it.walletConnectState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: WalletConnectState) { + if (activity == null || view == null) return + screenState.value = viewModel.updateState(state) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..71a9386b36 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -0,0 +1,180 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import androidx.compose.foundation.Image +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.common.extensions.getFromClipboard +import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold +import com.tangem.wallet.R + +@Composable +fun WalletConnectScreen( + state: WalletConnectScreenState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + SettingsScreensScaffold( + content = { + if (state.sessions.isEmpty()) { + EmptyScreen(state, modifier) + } else { + WalletConnectSessions(state, modifier) + } + }, + fab = { + if (!state.isLoading) { + AddSessionFab(onAddSession = { state.onAddSession(context.getFromClipboard()?.toString()) }) + } + }, + titleRes = R.string.wallet_connect_title, + onBackClick = onBackPressed, + ) +} + +@Composable +private fun AddSessionFab( + onAddSession: () -> Unit, + modifier: Modifier = Modifier, +) { + FloatingActionButton( + onClick = onAddSession, + backgroundColor = colorResource(id = R.color.button_primary), + contentColor = colorResource(id = R.color.icon_primary_2), + shape = RoundedCornerShape(16.dp), + modifier = modifier, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus), + contentDescription = "", + ) + } +} + +@Composable +private fun EmptyScreen(state: WalletConnectScreenState, modifier: Modifier = Modifier) { + if (state.isLoading) { + LinearProgressIndicator( + modifier = modifier.fillMaxWidth(), + color = colorResource(id = R.color.icon_accent), + ) + } + Column( + modifier = modifier + .fillMaxSize() + .padding(bottom = 64.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_walletconnect), + contentDescription = "", + colorFilter = ColorFilter.tint(colorResource(id = R.color.icon_inactive)), + contentScale = ContentScale.FillWidth, + modifier = modifier + .width(width = 100.dp), + ) + Spacer(modifier = modifier.size(24.dp)) + Text( + text = stringResource(id = R.string.wallet_connect_subtitle), + style = TangemTypography.body2, + color = colorResource(id = R.color.text_tertiary), + ) + } +} + +@Composable +private fun WalletConnectSessions( + state: WalletConnectScreenState, + modifier: Modifier = Modifier, +) { + if (state.isLoading) { + LinearProgressIndicator( + modifier = modifier + .fillMaxWidth() + .height(2.dp), + color = colorResource(id = R.color.icon_accent), + ) + } else { + Spacer(modifier = modifier.height(2.dp)) + } + + LazyColumn( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(state.sessions) { session -> + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = modifier) { + Text( + text = session.description, + style = TangemTypography.subtitle1, + color = colorResource(id = R.color.text_primary_1), + ) + } + IconButton( + onClick = { state.onRemoveSession(session.sessionId) }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_cross_rounded_24), + contentDescription = "", + tint = colorResource(id = R.color.icon_warning), + ) + } + } + } + } +} + +@Composable +@Preview +fun WalletConnectScreenPreview() { + WalletConnectScreen( + state = WalletConnectScreenState( + sessions = listOf( + WcSessionForScreen( + description = "session from some dApp", + cardId = "12312312321", + sessionId = "", + ), + ), + isLoading = true, + ), + {}, + ) +} diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt new file mode 100644 index 0000000000..44792f9623 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession + +data class WalletConnectScreenState( + val sessions: List, + val isLoading: Boolean = false, + val onRemoveSession: (String) -> Unit = {}, + val onAddSession: (String?) -> Unit = {}, +) + +data class WcSessionForScreen( + val description: String, + val cardId: String, + val sessionId: String, +) { + companion object { + fun fromSession(session: WalletConnectSession): WcSessionForScreen { + return WcSessionForScreen( + description = session.peerMeta.name, + cardId = session.wallet.cardId, + sessionId = session.session.toUri(), + ) + } + } +} + diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt deleted file mode 100644 index b019dc95e7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect - -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.transition.TransitionInflater -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentWalletConnectSessionsBinding -import org.rekotlin.StoreSubscriber - - -class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_sessions), - StoreSubscriber { - - private lateinit var walletConnectSessionsAdapter: WalletConnectSessionsAdapter - private val binding: FragmentWalletConnectSessionsBinding by viewBinding( - FragmentWalletConnectSessionsBinding::bind - ) - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback( - this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.walletConnectState == newState.walletConnectState - }.select { it.walletConnectState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.toolbar.setNavigationOnClickListener { - store.dispatch(NavigationAction.PopBackTo()) - } - setOnClickListeners() - setupTransactionsRecyclerView() - } - - private fun setOnClickListeners() { - binding.fabOpenSession.setOnClickListener { - store.dispatch(WalletConnectAction.StartWalletConnect(activity = requireActivity())) - } - } - - private fun setupTransactionsRecyclerView() = with(binding) { - walletConnectSessionsAdapter = WalletConnectSessionsAdapter() - rvWalletConnectSessions.layoutManager = LinearLayoutManager(requireContext()) - rvWalletConnectSessions.adapter = walletConnectSessionsAdapter - walletConnectSessionsAdapter.submitList(store.state.walletConnectState.sessions) - } - - override fun newState(state: WalletConnectState) { - if (activity == null || view == null) return - - binding.fabOpenSession.show(!state.loading) - binding.pbWalletConnect.show(state.loading) - - showUpdatedList(state.sessions) - } - - private fun showUpdatedList(sessions: List) = with(binding) { - walletConnectSessionsAdapter.submitList(sessions) - rvWalletConnectSessions.show(sessions.isNotEmpty()) - tvNoSessions.show(sessions.isEmpty()) - tvNoSessionsTitle.show(sessions.isEmpty()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt new file mode 100644 index 0000000000..6bbde4c1ae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.features.details.ui.walletconnect + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import org.rekotlin.Store + +class WalletConnectViewModel(private val store: Store) { + fun updateState(state: WalletConnectState): WalletConnectScreenState { + return WalletConnectScreenState( + state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) }, + isLoading = state.loading, + onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions) }, + onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, + ) + } + + private fun onRemoveSession(sessionUri: String, sessions: List) { + sessions + .firstOrNull { it.session.toUri() == sessionUri } + ?.let { baseSession -> + store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt index 84b9151436..b9fd651bb8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.tangem.blockchain.common.Blockchain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession @@ -10,26 +11,33 @@ import com.tangem.wallet.R class ApproveWcSessionDialog { companion object { - fun create(session: WalletConnectSession, context: Context): AlertDialog { + fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { val message = context.getString( R.string.wallet_connect_request_session_start, session.wallet.cardId, session.peerMeta.name, - session.peerMeta.url + session.peerMeta.url, ) return AlertDialog.Builder(context).apply { setTitle(context.getString(R.string.wallet_connect)) setMessage(message) setPositiveButton(context.getText(R.string.common_start)) { _, _ -> - store.dispatch(WalletConnectAction.ApproveSession( - session.session - )) + store.dispatch(GlobalAction.HideDialog) + store.dispatch(WalletConnectAction.ChooseNetwork(session.wallet.blockchain!!)) + } + if (networks.size > 1) { + setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + store.dispatch(WalletConnectAction.SelectNetwork(session = session, networks = networks)) + } } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> + store.dispatch(GlobalAction.HideDialog) store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) } - setOnDismissListener { + setOnCancelListener { store.dispatch(GlobalAction.HideDialog) + store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) } }.create() } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt new file mode 100644 index 0000000000..8e0ffe0f4a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import com.tangem.tap.store +import com.tangem.wallet.R + +object ChooseNetworkDialog { + fun create( + session: WalletConnectSession, + networks: List, + context: Context, + ): AlertDialog { + return AlertDialog.Builder(context) + .setTitle(context.getString(R.string.wallet_connect_select_network)) + .setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> + store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) + } + .setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + .setSingleChoiceItems(networks.map { it.fullName }.toTypedArray(), 0) { _, which -> + networks.getOrNull(which)?.let { selectedBlockchain -> + store.dispatch( + WalletConnectAction.ChooseNetwork( + blockchain = selectedBlockchain, + ), + ) + store.dispatch(GlobalAction.HideDialog) + } + } + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SimpleAlertDialog.kt deleted file mode 100644 index f4efd59532..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SimpleAlertDialog.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.store -import com.tangem.wallet.R - -class SimpleAlertDialog { - companion object { - fun create( - titleRes: Int, - messageRes: Int, - buttonRes: Int = R.string.common_ok, - context: Context, - ): AlertDialog { - - return AlertDialog.Builder(context).apply { - setTitle(context.getString(titleRes)) - setMessage(context.getText(messageRes)) - setPositiveButton(context.getText(buttonRes)) { _, _ -> } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index ad5a091c99..e69de29bb2 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -1,380 +0,0 @@ -package com.tangem.tap.features.feedback - -import android.app.Activity -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import com.tangem.Log -import com.tangem.TangemSdkLogger -import com.tangem.blockchain.common.* -import com.tangem.domain.common.ScanResponse -import com.tangem.domain.common.TapWorkarounds -import com.tangem.tap.common.extensions.sendEmail -import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.wallet.R -import timber.log.Timber -import java.io.File -import java.io.FileWriter -import java.io.StringWriter -import java.text.SimpleDateFormat -import java.util.* - - -/** -[REDACTED_AUTHOR] - */ -class FeedbackManager( - val infoHolder: AdditionalEmailInfo, - private val logCollector: TangemLogCollector, -) { - - private lateinit var activity: Activity - - fun updateActivity(activity: Activity) { - this.activity = activity - } - - fun send(emailData: EmailData, onFail: ((Exception) -> Unit)? = null) { - if (!this::activity.isInitialized) return - - emailData.prepare(infoHolder) - val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null - activity.sendEmail( - email = getSupportEmail(), - subject = activity.getString(emailData.subjectResId), - message = emailData.joinTogether(activity, infoHolder), - file = fileLog, - onFail = onFail - ) - } - - private fun getSupportEmail(): String { - return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) { - S2C_SUPPORT_EMAIL - } else { - DEFAULT_SUPPORT_EMAIL - } - } - - private fun createLogFile(): File? { - return try { - val file = File(activity.filesDir, "logs.txt") - file.delete() - file.createNewFile() - - val stringWriter = StringWriter() - logCollector.getLogs().forEach { stringWriter.append(it) } - val fileWriter = FileWriter(file) - fileWriter.write(stringWriter.toString()) - fileWriter.close() - logCollector.clearLogs() - file - } catch (ex: Exception) { - Timber.e(ex, "Can't create a file for email attachment") - null - } - } - - companion object { - const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" - const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" - } -} - -class TangemLogCollector : TangemSdkLogger { - private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS") - private val logs = mutableListOf() - private val mutex = Object() - - override fun log(message: () -> String, level: Log.Level) { - val time = dateFormatter.format(Date()) - synchronized(mutex) { - logs.add("$time: ${message()}\n") - } - } - - fun getLogs(): List = synchronized(mutex) { logs.toList() } - - fun clearLogs() { - synchronized(mutex) { logs.clear() } - } -} - -class AdditionalEmailInfo { - class EmailWalletInfo( - var blockchain: Blockchain = Blockchain.Unknown, - var address: String = "", - var explorerLink: String = "", - var host: String = "", - var derivationPath: String = "", - ) - - var appVersion: String = "" - - // card - var cardId: String = "" - var cardFirmwareVersion: String = "" - var cardIssuer: String = "" - var cardBlockchain: String = "" - - // wallets - internal val walletsInfo = mutableListOf() - internal val tokens = mutableMapOf>() - internal var onSendErrorWalletInfo: EmailWalletInfo? = null - var signedHashesCount: String = "" - - // device - var phoneModel: String = Build.MODEL - var osVersion: String = Build.VERSION.SDK_INT.toString() - - // send error - var destinationAddress: String = "" - var amount: String = "" - var fee: String = "" - var token: String = "" - - fun setAppVersion(context: Context) { - try { - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - appVersion = pInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - } - } - - fun setCardInfo(data: ScanResponse) { - cardId = data.card.cardId - cardBlockchain = data.walletData?.blockchain ?: "" - cardFirmwareVersion = data.card.firmwareVersion.stringValue - cardIssuer = data.card.issuer.name - signedHashesCount = data.card.wallets - .joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" } - } - - 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 ?: "" - ) - ) - if (manager.cardTokens.isNotEmpty()) { - tokens[manager.wallet.blockchain] = manager.cardTokens - } - } - } - - fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) { - onSendErrorWalletInfo = EmailWalletInfo( - blockchain = wallet.blockchain, - address = getAddress(wallet), - explorerLink = getExploreUri(wallet), - host = host, - derivationPath = wallet.publicKey.derivationPath?.rawPath ?: "" - ) - - 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 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" - } - } -} - -interface EmailData { - val subjectResId: Int - val mainMessageResId: Int - - fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message - - fun prepare(infoHolder: AdditionalEmailInfo) {} - - fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String - - fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String { - return StringBuilder().apply { - append(context.getString(mainMessageResId)) - appendLine(3) - append(context.getString(getDataCollectionMessageResId())) - appendLine() - append(createOptionalMessage(infoHolder)) - }.toString() - } -} - -class RateCanBeBetterEmail : EmailData { - override val subjectResId: Int = R.string.feedback_subject_rate_negative - override val mainMessageResId: Int = R.string.feedback_preface_rate_negative - - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) - .appendCardInfo() - .appendLine() - .appendPhoneInfo() - .build() -} - -class ScanFailsEmail : EmailData { - - override val subjectResId: Int = R.string.feedback_subject_scan_failed - override val mainMessageResId: Int = R.string.feedback_preface_scan_failed - - override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String = StringBuilder().apply { - append(context.getString(mainMessageResId)) - appendLine(4) - append(createOptionalMessage(infoHolder)) - }.toString() - - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) - .appendPhoneInfo() - .build() -} - -class SendTransactionFailedEmail( - val error: String -) : EmailData { - - override val subjectResId: Int = R.string.feedback_subject_tx_failed - override val mainMessageResId: Int = R.string.feedback_preface_tx_failed - - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) - .appendCardInfo() - .appendDelimiter() - .appendTxFailedBlockchainInfo(error) - .appendLine() - .appendPhoneInfo() - .build() -} - -class FeedbackEmail : EmailData { - override val subjectResId: Int - get() = if (isS2CCard) s2cSubject else tangemSubject - override val mainMessageResId: Int - get() = if (isS2CCard) s2cMainMessage else tangemMainMessage - - private val tangemSubject: Int = R.string.feedback_subject_support_tangem - private val tangemMainMessage: Int = R.string.feedback_preface_support - - private val s2cSubject: Int = R.string.feedback_subject_support - private val s2cMainMessage: Int = R.string.feedback_preface_support - - private var isS2CCard = false - - override fun prepare(infoHolder: AdditionalEmailInfo) { - isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer) - } - - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) - .appendCardInfo() - .appendWalletsInfo() - .appendLine() - .appendPhoneInfo() - .build() -} - - -class EmailDataBuilder( - private val infoHolder: AdditionalEmailInfo -) { - val builder = StringBuilder() - - fun appendDelimiter(): EmailDataBuilder { - builder.appendDelimiter() - return this - } - - fun appendLine(count: Int = 1): EmailDataBuilder { - builder.appendLine(count) - return this - } - - fun appendCardInfo(): EmailDataBuilder { - builder.appendKeyValue("Card ID", infoHolder.cardId) - builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) - builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain) - builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount) - return this - } - - fun appendWalletsInfo(): EmailDataBuilder { - 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) - - infoHolder.tokens[it.blockchain]?.let { tokens -> - builder.append("Tokens:") - appendLine() - tokens.forEach { token -> - builder.appendKeyValue("Name", token.name) - builder.appendKeyValue("ID", token.id ?: "[custom token]") - builder.appendKeyValue("Contract address", token.contractAddress) - } - } - } - return this - } - - fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder { - val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo() - builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName) - builder.appendKeyValue("Derivation path", walletInfo.derivationPath) - builder.appendKeyValue("Host", walletInfo.host) - builder.appendKeyValue("Token", infoHolder.token) - builder.appendKeyValue("Error", error) - builder.appendDelimiter() - builder.appendKeyValue("Source address", walletInfo.address) - builder.appendKeyValue("Destination address", infoHolder.destinationAddress) - builder.appendKeyValue("Amount", infoHolder.amount) - builder.appendKeyValue("Fee", infoHolder.fee) - return this - } - - fun appendPhoneInfo(): EmailDataBuilder { - builder.appendKeyValue("Phone model", infoHolder.phoneModel) - builder.appendKeyValue("OS version", infoHolder.osVersion) - builder.appendKeyValue("App version", infoHolder.appVersion) - return this - } - - 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.appendDelimiter(): StringBuilder = append("----------\n") - -private fun StringBuilder.appendLine(count: Int = 1): StringBuilder { - return append(List(count) { "\n" }.joinToString(separator = "")) -} \ 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 c453cdadc4..27953640ea 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 @@ -1,10 +1,14 @@ package com.tangem.tap.features.home import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import com.google.accompanist.appcompattheme.AppCompatTheme import com.tangem.tap.common.redux.navigation.AppScreen @@ -15,48 +19,36 @@ import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.store -import com.tangem.wallet.R import org.rekotlin.StoreSubscriber -class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber { +class HomeFragment : Fragment(), StoreSubscriber { - var homeState: MutableState = mutableStateOf(store.state.homeState) + private var homeState: MutableState = mutableStateOf(store.state.homeState) + private var composeView: ComposeView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) store.dispatch(HomeAction.Init) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + val context = container?.context ?: return null store.dispatch(BackupAction.CheckForUnfinishedBackup) - - - getView()?.findViewById(R.id.cv_stories)?.setContent { - AppCompatTheme { - StoriesScreen( - homeState, - onScanButtonClick = { store.dispatch(HomeAction.ReadCard) }, - 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()) - } - ) + composeView = ComposeView(context).apply { + setContent { + AppCompatTheme { + ScreenContent() + } } } - } - private fun getRegionProvider(): RegionProvider = RegionService( - listOf( -// TelephonyManagerRegionProvider(requireContext()), - LocaleRegionProvider() - ) - ) + return composeView + } override fun onStart() { super.onStart() @@ -72,6 +64,11 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber Unit -) { - val screenState = remember { mutableStateOf(StartingScreenState.INIT) } - val progress = remember { Animatable(0f) } - - LaunchedEffect(paused) { - if (paused) { - progress.stop() - } else { - progress.animateTo( - targetValue = 2f, - animationSpec = tween( - durationMillis = duration, - easing = LinearEasing - ) - ) - } - } - - when (progress.value) { - in 0f..0.2f -> screenState.value = StartingScreenState.INIT - in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY - in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE - in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND - in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY - in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE - in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW - in 0.8f..1f -> screenState.value = StartingScreenState.LEND - in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD - in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM - } - - if (screenState.value == StartingScreenState.INIT) hideContent(true) - if (screenState.value == StartingScreenState.BUY) hideContent(false) - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = - Modifier - .fillMaxSize(), - ) { - - val text = when (screenState.value) { - StartingScreenState.INIT -> null - StartingScreenState.BUY -> R.string.story_meet_buy - StartingScreenState.STORE -> R.string.story_meet_store - StartingScreenState.SEND -> R.string.story_meet_send - StartingScreenState.PAY -> R.string.story_meet_pay - StartingScreenState.EXCHANGE -> R.string.story_meet_exchange - StartingScreenState.BORROW -> R.string.story_meet_borrow - StartingScreenState.LEND -> R.string.story_meet_lend - StartingScreenState.SHOW_CARD -> R.string.story_meet_title - StartingScreenState.MEET_TANGEM -> R.string.story_meet_title - } - - val style = TextStyle( - fontSize = 60.sp, - fontWeight = FontWeight.SemiBold, - color = Color.White, - textAlign = TextAlign.Center, - ) - - if (screenState.value != StartingScreenState.MEET_TANGEM) { - TextAutoSize( - modifier = Modifier - .padding(start = 20.dp, end = 20.dp, bottom = 100.dp) - .alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f), - text = text?.let { stringResource(text) } ?: "", - textStyle = style, - fontSizeRange = FontSizeRange(20.sp, 60.sp) - ) - } - - AnimatedVisibility( - visible = screenState.value == StartingScreenState.MEET_TANGEM, - enter = slideInVertically() { it / 2 } - ) { - TextAutoSize( - modifier = Modifier - .fillMaxWidth() - .padding(start = 40.dp, end = 40.dp), - text = text?.let { stringResource(text) } ?: "", - textStyle = style, - fontSizeRange = FontSizeRange(20.sp, 60.sp) - ) - } - - AnimatedVisibility( - visible = screenState.value == StartingScreenState.SHOW_CARD || - screenState.value == StartingScreenState.MEET_TANGEM, - enter = scaleIn(initialScale = 3f) - ) { - Image( - painter = painterResource( - id = R.drawable.meet_tangem - ), - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - } - } -} - -enum class StartingScreenState { - INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM -} - - -@Preview -@Composable -fun Stories1Preview() { - FirstStoriesContent(false, 7_000) {} -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt new file mode 100644 index 0000000000..58f9356349 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt @@ -0,0 +1,152 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.animation.core.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +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.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.extensions.AnimatedValue +import com.tangem.tap.common.compose.extensions.toAnimatable + +@Composable +fun HorizontalSlidingImage( + painter: Painter, + paused: Boolean, + duration: Int, + itemSize: DpSize, + startOffset: Float, + targetOffset: Float, + contentDescription: String, +) { + val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f) + + Image( + modifier = Modifier + .requiredWidth(itemSize.width) + .requiredHeight(itemSize.height) + .graphicsLayer( + translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value + ), + alignment = Alignment.TopStart, + contentScale = ContentScale.FillBounds, + painter = painter, + contentDescription = contentDescription, + ) +} + +@Composable +fun StoriesTextAnimation( + slideInDuration: Int = 500, + slideInDelay: Int = 200, + slideDistance: Dp = 60.dp, + label: String = "", + content: @Composable (Modifier) -> Unit +) { + val isLaunched = remember { mutableStateOf(false) } + val transition = updateTransition(targetState = isLaunched.value, label = label) + + val offsetY = transition.animateDp( + transitionSpec = { + tween( + durationMillis = slideInDuration, + delayMillis = slideInDelay, + easing = FastOutSlowInEasing + ) + }, + label = "Slide in" + ) { value -> if (value) 0.dp else slideDistance } + + val alpha = transition.animateFloat( + transitionSpec = { + tween( + durationMillis = slideInDuration * 2, + delayMillis = slideInDelay, + easing = FastOutSlowInEasing + ) + }, + label = "Visibility" + ) { value -> if (value) 1f else 0f } + + content( + Modifier + .absoluteOffset(y = offsetY.value) + .alpha(alpha.value) + ) + + LaunchedEffect(Unit) { isLaunched.value = true } +} + +@Composable +fun StoriesBottomImageAnimation( + initialScale: Float = 2.5f, + firstStepDuration: Int, + totalDuration: Int, + content: @Composable (Modifier) -> Unit +) { + val scaleSwitchBarrier = 1.15f + val secondStepDuration = totalDuration - firstStepDuration + + val isFirstStepLaunched = remember { mutableStateOf(false) } + val isSecondStepLaunched = remember { mutableStateOf(false) } + + val firstTransition = updateTransition( + targetState = isFirstStepLaunched.value, + label = "Image appearing" + ) + val firstScaleStep = firstTransition.animateFloat( + transitionSpec = { + tween( + durationMillis = firstStepDuration, + easing = FastOutLinearInEasing, + ) + }, + label = "Appearing scale" + ) { value -> if (value) scaleSwitchBarrier else initialScale } + + val secondTransition = updateTransition( + targetState = isSecondStepLaunched.value, + label = "Image slow outgoing" + ) + val secondScaleStep = secondTransition.animateFloat( + transitionSpec = { + tween( + durationMillis = secondStepDuration, + easing = LinearEasing, + ) + }, + label = "Outgoing scale" + ) { value -> if (value) 1f else scaleSwitchBarrier } + + val fadeIn = firstTransition.animateFloat( + transitionSpec = { tween(durationMillis = 400) }, + label = "Fade in on start" + ) { value -> if (value) 1f else 0f } + + if (firstScaleStep.value == scaleSwitchBarrier) { + isSecondStepLaunched.value = true + } + + val modifier = if (!isSecondStepLaunched.value) { + Modifier.scale(firstScaleStep.value) + } else { + Modifier.scale(secondScaleStep.value) + }.alpha(fadeIn.value) + + content(modifier) + + LaunchedEffect(Unit) { isFirstStepLaunched.value = true } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt deleted file mode 100644 index 410a8118aa..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.tangem.tap.features.home.compose - -import android.content.Context -import android.graphics.Typeface -import android.util.TypedValue -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import com.tangem.tangem_sdk_new.extensions.dpToPx -import com.tangem.tap.common.compose.SpacerS16 -import com.tangem.tap.common.compose.SpacerS24 -import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor -import com.tangem.wallet.R - -@Composable -fun StoriesGeneralContent( - titleText: String, - subtitleText: String, - imageSource: Int?, - isDarkBackground: Boolean, - subtitleTextId: Int? = null, - imageComposable: (() -> Unit)? = null, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight(), - ) { - - Text( - text = titleText, - fontSize = 32.sp, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .padding(start = 40.dp, end = 40.dp), - color = if (isDarkBackground) Color.White else Color(0xFF090E13), - textAlign = TextAlign.Center - ) - - SpacerS16() - - SubtitleText(subtitleText, subtitleTextId) - - SpacerS24() - - if (imageSource != null) { - Image( - painter = painterResource(id = imageSource), - contentDescription = null, - contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - } - imageComposable?.invoke() - } -} - -@Composable -fun SubtitleText(subtitleText: String, subtitleTextId: Int?) { - val color = Color(0xFFA6AAAD) - - if (subtitleTextId == null) { - Text( - text = subtitleText, - fontSize = 20.sp, - fontWeight = FontWeight.Normal, - modifier = Modifier.padding(start = 40.dp, end = 40.dp), - color = color, - textAlign = TextAlign.Center - ) - } else { - HtmlText(subtitleTextId) { context -> - val padding = context.dpToPx(40f).toInt() - TextView(context).apply { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - setPadding(padding, 0, padding, 0) - textAlignment = View.TEXT_ALIGNMENT_CENTER - typeface = Typeface.DEFAULT - setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f) - setTextColor(color.toAndroidGraphicsColor()) - } - } - } -} - -@Composable -fun StoriesRevolutionaryWallet() { - StoriesGeneralContent( - titleText = stringResource(id = R.string.story_awe_title), - subtitleText = stringResource(id = R.string.story_awe_description), - imageSource = R.drawable.revolutionary_wallet, - isDarkBackground = true - ) -} - -@Composable -fun StoriesUltraSecureBackup() { - StoriesGeneralContent( - titleText = stringResource(id = R.string.story_backup_title), - subtitleText = stringResource(id = R.string.story_backup_description), - imageSource = R.drawable.floating_cards, - subtitleTextId = R.string.story_backup_description, - isDarkBackground = false - ) -} - -@Composable -fun StoriesThousandsOfCurrencies() { - StoriesGeneralContent( - titleText = stringResource(id = R.string.story_currencies_title), - subtitleText = stringResource(id = R.string.story_currencies_description), - imageSource = R.drawable.thousands_of_currencies, - isDarkBackground = false - ) -} - -@Composable -fun StoriesWeb3() { - StoriesGeneralContent( - titleText = stringResource(id = R.string.story_web3_title), - subtitleText = stringResource(id = R.string.story_web3_description), - imageSource = R.drawable.web_3, - isDarkBackground = false - ) -} - -@Composable -fun StoriesWalletForEveryone() { - StoriesGeneralContent( - titleText = stringResource(id = R.string.story_finish_title), - subtitleText = stringResource(id = R.string.story_finish_description), - imageSource = R.drawable.wallet_for_everyone, - isDarkBackground = true - ) -} - -@Composable -fun HtmlText( - stringResId: Int, - modifier: Modifier = Modifier, - factory: (Context) -> TextView -) { - AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt similarity index 68% rename from app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt rename to app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index 4bfac1e8a0..21adc5e516 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -3,11 +3,25 @@ package com.tangem.tap.features.home.compose import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -24,8 +38,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.tap.common.compose.SpacerS24 import androidx.compose.ui.unit.sp +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.tap.features.home.compose.content.FirstStoriesContent +import com.tangem.tap.features.home.compose.content.StoriesCurrencies +import com.tangem.tap.features.home.compose.content.StoriesRevolutionaryWallet +import com.tangem.tap.features.home.compose.content.StoriesUltraSecureBackup +import com.tangem.tap.features.home.compose.content.StoriesWalletForEveryone +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 @@ -39,9 +61,8 @@ fun StoriesScreen( onSearchTokensClick: () -> Unit, ) { val steps = 6 - val stepDuration = 8_000 - val currentStep = remember { mutableStateOf(1) } + val systemUiController = rememberSystemUiController() val isDarkBackground = currentStep.value !in 3..5 @@ -53,19 +74,24 @@ fun StoriesScreen( } val isPressed = remember { mutableStateOf(false) } - val needsToBePaused = - remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) } - val pause = isPressed.value || needsToBePaused.value + val isPaused = isPressed.value || homeState.value.btnScanState.progressState == ProgressState.Loading val hideContent = remember { mutableStateOf(true) } + LaunchedEffect(key1 = isDarkBackground) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = !isDarkBackground, + ) + } + Box( - Modifier + modifier = Modifier .fillMaxSize() .background(Color(0xFF090E13)) ) { Row( - Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize() ) { Box( Modifier @@ -106,25 +132,28 @@ fun StoriesScreen( } if (!isDarkBackground) { Image( + modifier = Modifier.fillMaxSize(), painter = painterResource(id = R.drawable.ic_overlay), contentDescription = null, - modifier = Modifier - .fillMaxSize(), contentScale = ContentScale.FillBounds ) } + + val statusBarInsets = WindowInsets.statusBars + .union(WindowInsets(top = 32.dp)) + Column( + modifier = Modifier + .windowInsetsPadding(statusBarInsets) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() ) { - SpacerS24() StoriesProgressBar( steps = steps, currentStep = currentStep.value, -// paused = isPressed.value, - stepDuration = stepDuration, - paused = pause, + stepDuration = currentStep.duration(), + paused = isPaused, onStepFinished = goToNextScreen, ) Image( @@ -139,26 +168,26 @@ fun StoriesScreen( colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black) ) when (currentStep.value) { - 1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it } - 2 -> StoriesRevolutionaryWallet() - 3 -> StoriesUltraSecureBackup() - 4 -> StoriesThousandsOfCurrencies() - 5 -> StoriesWeb3() - 6 -> StoriesWalletForEveryone() + 1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it } + 2 -> StoriesRevolutionaryWallet(currentStep.duration()) + 3 -> StoriesUltraSecureBackup(isPaused, currentStep.duration()) + 4 -> StoriesCurrencies(isPaused, currentStep.duration()) + 5 -> StoriesWeb3(isPaused, currentStep.duration()) + 6 -> StoriesWalletForEveryone(currentStep.duration()) } } Column( modifier = Modifier + .navigationBarsPadding() .align(Alignment.BottomCenter) - .fillMaxWidth() + .fillMaxWidth(), ) { if (currentStep.value == 4) Button( onClick = onSearchTokensClick, modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .height(48.dp) - , + .height(48.dp), colors = ButtonDefaults.textButtonColors( backgroundColor = Color.White, contentColor = Color(0xFF080C10) @@ -184,13 +213,16 @@ fun StoriesScreen( onShopButtonClick = onShopButtonClick ) } - } } +private fun MutableState.duration(): Int = when (this.value) { + 1 -> 8000 + else -> 6000 +} @Preview @Composable -fun InstagramScreenPreview() { +fun StoriesScreenPreview() { StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt new file mode 100644 index 0000000000..9ee80263c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt @@ -0,0 +1,263 @@ +package com.tangem.tap.features.home.compose.content + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.SpacerH16 +import com.tangem.tap.common.compose.SpacerH32 +import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation +import com.tangem.tap.features.home.compose.StoriesTextAnimation +import com.tangem.wallet.R + +@Composable +fun StoriesRevolutionaryWallet(stepDuration: Int) { + SplitContent( + topContent = { + TopContent( + titleText = stringResource(id = R.string.story_awe_title), + subtitleText = stringResource(id = R.string.story_awe_description).annotated(), + isDarkBackground = true, + ) + }, + bottomContent = { + StoriesBottomImageAnimation( + totalDuration = stepDuration, + firstStepDuration = 300, + ) { modifier -> + StoriesImage( + modifier = modifier, + drawableResId = R.drawable.revolutionary_wallet, + isDarkBackground = true, + ) + } + } + ) +} + +@Composable +fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) { + val subtitleText = buildAnnotatedString { + append(stringResource(id = R.string.story_backup_description_1)) + append(" ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(stringResource(id = R.string.story_backup_description_2_bold)) + } + append(" ") + append(stringResource(id = R.string.story_backup_description_3)) + } + SplitContent( + topContent = { + TopContent( + titleText = stringResource(id = R.string.story_backup_title), + subtitleText = subtitleText, + isDarkBackground = false, + ) + }, + bottomContent = { + FloatingCardsContent(isPaused, stepDuration) + } + ) +} + +@Composable +fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) { + SplitContent( + topContent = { + TopContent( + titleText = stringResource(id = R.string.story_currencies_title), + subtitleText = stringResource(id = R.string.story_currencies_description).annotated(), + isDarkBackground = false, + ) + }, + bottomContent = { + StoriesCurrenciesContent(paused = isPaused, duration = stepDuration) + } + ) +} + +@Composable +fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) { + SplitContent( + topContent = { + TopContent( + titleText = stringResource(id = R.string.story_web3_title), + subtitleText = stringResource(id = R.string.story_web3_description).annotated(), + isDarkBackground = false, + ) + }, + bottomContent = { + StoriesWeb3Content(paused = isPaused, duration = stepDuration) + } + ) +} + +@Composable +fun StoriesWalletForEveryone(stepDuration: Int) { + SplitContent( + topContent = { + TopContent( + titleText = stringResource(id = R.string.story_finish_title), + subtitleText = stringResource(id = R.string.story_finish_description).annotated(), + isDarkBackground = true, + ) + }, + bottomContent = { + StoriesBottomImageAnimation( + totalDuration = stepDuration, + firstStepDuration = 500, + ) { modifier -> + StoriesImage( + modifier = modifier, + drawableResId = R.drawable.wallet_for_everyone, + isDarkBackground = true, + ) + } + } + ) +} + +@Composable +private fun SplitContent( + topContent: @Composable () -> Unit, + bottomContent: @Composable () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + topContent() + bottomContent() + } +} + +@Composable +private fun TopContent( + titleText: String, + subtitleText: AnnotatedString, + isDarkBackground: Boolean, +) { + SpacerH32() + StoriesTitleText( + text = titleText, + isDarkBackground = isDarkBackground, + ) + SpacerH16() + StoriesSubtitleText( + subtitleText = subtitleText, + ) + SpacerH32() +} + +@Composable +private fun StoriesTitleText( + text: String, + isDarkBackground: Boolean, +) { + StoriesTextAnimation( + slideInDuration = 500, + slideInDelay = 150, + ) { modifier -> + Text( + modifier = modifier + .padding(start = 40.dp, end = 40.dp), + text = text, + fontSize = 32.sp, + fontWeight = FontWeight.SemiBold, + color = if (isDarkBackground) Color.White else Color(0xFF090E13), + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun StoriesSubtitleText( + subtitleText: AnnotatedString, +) { + val color = Color(0xFFA6AAAD) + + StoriesTextAnimation( + slideInDuration = 500, + slideInDelay = 400, + ) { modifier -> + Text( + modifier = modifier + .padding(start = 40.dp, end = 40.dp), + fontWeight = FontWeight.Normal, + text = subtitleText, + fontSize = 20.sp, + color = color, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun StoriesImage( + modifier: Modifier = Modifier, + @DrawableRes drawableResId: Int, + isDarkBackground: Boolean, +) { + Image( + painter = painterResource(id = drawableResId), + contentDescription = null, + contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, + modifier = modifier.fillMaxWidth() + ) +} + +private fun String.annotated(): AnnotatedString { + val source = this + return buildAnnotatedString { append(source) } +} + + +@Preview +@Composable +private fun RevolutionaryWalletPreview() { + StoriesRevolutionaryWallet(6000) +} + +@Preview +@Composable +private fun UltraSecureBackupPreview() { + StoriesUltraSecureBackup(false, 6000) +} + +@Preview +@Composable +private fun CurrenciesPreview() { + StoriesCurrencies(false, 6000) +} + + +@Preview +@Composable +private fun Web3Preview() { + StoriesWeb3(false, 6000) +} + + +@Preview +@Composable +private fun WalletForEveryonePreview() { + StoriesWalletForEveryone(6000) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt new file mode 100644 index 0000000000..198e71d324 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt @@ -0,0 +1,144 @@ +package com.tangem.tap.features.home.compose.content + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.SpacerH +import com.tangem.tap.common.compose.extensions.dpSize +import com.tangem.tap.common.compose.extensions.halfHeight +import com.tangem.tap.common.compose.extensions.toPx +import com.tangem.tap.common.extensions.isEven +import com.tangem.tap.features.home.compose.HorizontalSlidingImage +import com.tangem.wallet.R + +@Composable +fun StoriesCurrenciesContent( + paused: Boolean, + duration: Int, +) { + val currencyDrawableList = remember { + listOf( + R.drawable.currency0, + R.drawable.currency1, + R.drawable.currency2, + R.drawable.currency3, + R.drawable.currency4, + ) + } + + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val decreaseRate = remember { 1f / currencyDrawableList.size } + val designItemHeight = remember { 82.dp } + + LightenBox { + Column(modifier = Modifier.graphicsLayer(clip = false)) { + currencyDrawableList.forEachIndexed { index, drawableResId -> + val painter = painterResource(id = drawableResId) + val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight) + val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth + val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2 + + val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight() + val animateFrom = chessOffset - moveItemToStartOfScreen + val animateTo = 50.dp - (50.dp * index * decreaseRate) + + HorizontalSlidingImage( + paused = paused, + duration = duration, + painter = painter, + itemSize = scaledItemSize, + startOffset = animateFrom.toPx(), + targetOffset = animateTo.toPx(), + contentDescription = "Currency row", + ) + SpacerH(12.dp) + } + } + } +} + +@Composable +fun StoriesWeb3Content( + paused: Boolean, + duration: Int, +) { + val dappsItemList = remember { + listOf( + R.drawable.dapps0, + R.drawable.dapps1, + R.drawable.dapps2, + R.drawable.dapps3, + R.drawable.dapps4, + R.drawable.dapps5, + ) + } + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val decreaseRate = remember { 1f / dappsItemList.size } + val designItemHeight = 75.dp + + LightenBox { + Column(modifier = Modifier.graphicsLayer(clip = false)) { + dappsItemList.forEachIndexed { index, drawableResId -> + val painter = painterResource(id = drawableResId) + val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight) + val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth + val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2 + + val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3 + val animateFrom = chessOffset - moveItemToStartOfScreen + val animateTo = 70.dp - (70.dp * index * decreaseRate) + + HorizontalSlidingImage( + paused = paused, + duration = duration, + painter = painter, + itemSize = scaledItemSize, + startOffset = animateFrom.toPx(), + targetOffset = animateTo.toPx(), + contentDescription = "Web3 row", + ) + } + } + + } +} + +@Composable +private fun LightenBox(content: @Composable () -> Unit) { + Box() { + content() + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 250.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = 0f), + Color.White.copy(alpha = 0.75f), + Color.White.copy(alpha = 0.95f), + Color.White + ) + ) + ) + ) {} + } +} + +private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize { + val scaleRate = itemSize.height / designItemHeight + return itemSize / scaleRate +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt new file mode 100644 index 0000000000..393642f976 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt @@ -0,0 +1,163 @@ +package com.tangem.tap.features.home.compose.content + +import androidx.annotation.StringRes +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.FontSizeRange +import com.tangem.tap.common.compose.TextAutoSize +import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation +import com.tangem.tap.features.home.compose.StoriesTextAnimation +import com.tangem.wallet.R + +@Composable +fun FirstStoriesContent( + isPaused: Boolean, duration: Int = 8_000, + hideContent: (Boolean) -> Unit +) { + val screenState = remember { mutableStateOf(StartingScreenState.INIT) } + val progress = remember { Animatable(0f) } + + LaunchedEffect(isPaused) { + if (isPaused) { + progress.stop() + } else { + progress.animateTo( + targetValue = 2f, + animationSpec = tween( + durationMillis = duration, + easing = LinearEasing + ) + ) + } + } + + when (progress.value) { + in 0f..0.2f -> screenState.value = StartingScreenState.INIT + in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY + in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE + in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND + in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY + in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE + in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW + in 0.8f..1f -> screenState.value = StartingScreenState.LEND + in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD + in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM + } + + if (screenState.value == StartingScreenState.INIT) hideContent(true) + if (screenState.value == StartingScreenState.BUY) hideContent(false) + + val style = TextStyle( + fontSize = 60.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White, + textAlign = TextAlign.Center, + ) + val textId = screenState.textId() + + Box(modifier = Modifier.fillMaxSize()) { + + if (screenState.isSplashingTextDisplaying()) { + TextAutoSize( + modifier = Modifier + .align(Alignment.Center) + .padding(start = 20.dp, end = 20.dp, bottom = 100.dp), + text = textId?.let { stringResource(textId) } ?: "", + textStyle = style, + fontSizeRange = FontSizeRange(20.sp, 60.sp) + ) + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .weight(0.7f) + ) { + if (screenState.isMeetTangemDisplaying()) { + StoriesTextAnimation( + slideInDelay = 0, + ) { modifier -> + TextAutoSize( + modifier = modifier + .padding(start = 20.dp, end = 20.dp, top = 50.dp) + .alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f), + text = textId?.let { stringResource(textId) } ?: "", + textStyle = style, + fontSizeRange = FontSizeRange(30.sp, 50.sp) + ) + } + + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1.2f) + .wrapContentSize(), + ) { + StoriesBottomImageAnimation( + totalDuration = duration, + firstStepDuration = 400, + ) { modifier -> + Image( + modifier = modifier.fillMaxWidth(), + painter = painterResource(id = R.drawable.meet_tangem), + contentDescription = "Tangem Wallet card", + ) + } + } + } + } + } +} + +private enum class StartingScreenState { + INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM +} + +@StringRes +private fun MutableState.textId(): Int? = when (this.value) { + StartingScreenState.INIT -> null + StartingScreenState.BUY -> R.string.story_meet_buy + StartingScreenState.STORE -> R.string.story_meet_store + StartingScreenState.SEND -> R.string.story_meet_send + StartingScreenState.PAY -> R.string.story_meet_pay + StartingScreenState.EXCHANGE -> R.string.story_meet_exchange + StartingScreenState.BORROW -> R.string.story_meet_borrow + StartingScreenState.LEND -> R.string.story_meet_lend + StartingScreenState.SHOW_CARD -> R.string.story_meet_title + StartingScreenState.MEET_TANGEM -> R.string.story_meet_title +} + +private fun MutableState.isSplashingTextDisplaying(): Boolean { + return this.value != StartingScreenState.MEET_TANGEM && + this.value != StartingScreenState.SHOW_CARD +} + +private fun MutableState.isMeetTangemDisplaying(): Boolean { + return this.value == StartingScreenState.MEET_TANGEM +} + + +@Preview +@Composable +fun FirstStoriesPreview() { + FirstStoriesContent(false, 8000) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt new file mode 100644 index 0000000000..8528982857 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.features.home.compose.content + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import com.tangem.tap.common.compose.extensions.AnimatedValue +import com.tangem.tap.common.compose.extensions.asImageBitmap +import com.tangem.tap.common.compose.extensions.toAnimatable +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun FloatingCardsContent( + isPaused: Boolean, + stepDuration: Int, +) { + + val imageBitmap = asImageBitmap(R.drawable.card_placeholder_wallet) + val cards = listOf( + FloatingCard.first(), + FloatingCard.second(), + FloatingCard.third(), + ) + Box() { + cards.forEach { floatingCard -> + FloatingCard.Item( + isPaused = isPaused, + imageBitmap = imageBitmap, + cardValues = floatingCard, + stepDuration = stepDuration, + ) + } + } +} + +private data class CardValues( + val translateX: AnimatedValue = AnimatedValue(0f, 0f), + val translateY: AnimatedValue = AnimatedValue(0f, 0f), + val rotationX: AnimatedValue = AnimatedValue(0f, 0f), + val rotationY: AnimatedValue = AnimatedValue(0f, 0f), + val rotationZ: AnimatedValue = AnimatedValue(0f, 0f), + val scale: AnimatedValue = AnimatedValue(1f, 1f), +) + +private class FloatingCard { + companion object { + + @Composable + fun Item( + isPaused: Boolean, + stepDuration: Int, + imageBitmap: ImageBitmap, + cardValues: CardValues, + ) { + Image( + bitmap = imageBitmap, + contentDescription = "Floating Tangem card", + modifier = Modifier + .graphicsLayer( + translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value, + translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value, + rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value, + rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value, + rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value, + scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value, + scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value, + ), + ) + } + + fun first(): CardValues = CardValues( + translateX = -400f to -350f, + translateY = 30f to 32f, + rotationX = 10f to 15f, + rotationY = 15f to 15f, + rotationZ = 40f to 27f, + scale = 0.6f to 0.6f, + ) + + fun second(): CardValues = CardValues( + translateX = 350f to 300f, + translateY = -70f to 0f, + rotationX = 30f to 48f, + rotationY = 0f to 5f, + rotationZ = -34f to -42f, + scale = 0.47f to 0.35f, + ) + + fun third(): CardValues = CardValues( + translateX = 320f to 250f, + translateY = 500f to 500f, + rotationX = 0f to 3f, + rotationY = 10f to 10f, + rotationZ = -45f to -30f, + scale = 0.6f to 0.75f, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt rename to app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 04fb1f2647..d49013db87 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose +package com.tangem.tap.features.home.compose.views import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt rename to app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt index d121949529..096b87cb0b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose +package com.tangem.tap.features.home.compose.views import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt index e9f47acef9..50faf24376 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt @@ -10,8 +10,8 @@ import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchShare import com.tangem.tap.common.extensions.dispatchToastNotification import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.AddressData -import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index a539cefefd..8284efcd3d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -12,8 +12,8 @@ import com.tangem.tap.common.extensions.isPositive import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.hasPendingTransactions -import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.persistence.UsedCardsPrefStorage import timber.log.Timber 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 2c854ab5a0..34c0d381b2 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 @@ -15,7 +15,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.scope import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt index a9ebb0e757..ae7a2db594 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt @@ -26,7 +26,7 @@ data class OnboardingNoteState( get() = steps.indexOf(currentStep) val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false + store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 0f854da103..e470680dbc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.onboarding.products.otherCards.redux +import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE @@ -10,6 +11,8 @@ 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.domain.extensions.hasWallets +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -73,6 +76,21 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { onboardingManager.scanResponse = updatedResponse onboardingManager.activationStarted(updatedResponse.card.cardId) + val primaryBlockchain = updatedResponse.getBlockchain() + val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) { + val primaryToken = updatedResponse.getPrimaryToken() + val blockchainNetwork = BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens( + listOfNotNull(primaryToken)) + listOf(blockchainNetwork) + } else { + listOf( + BlockchainNetwork(Blockchain.Bitcoin, updatedResponse.card), + BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card) + ) + } + + store.dispatch(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks)) + delay(DELAY_SDK_DIALOG_CLOSE) store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done)) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 3ba1648a4b..0dc7e10566 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -6,17 +6,23 @@ import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse 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 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.currenciesRepository import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.twins.TwinCardsManager -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope @@ -265,7 +271,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) } CreateTwinWalletMode.RecreateWallet -> { - store.dispatch(NavigationAction.PopBackTo()) + currenciesRepository.removeCurrencies(scanResponse.card.cardId) + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index 19cc8ad307..0b2e4b4d1a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -56,7 +56,7 @@ data class TwinCardsState( get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false + store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) } } 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 c880488141..391d65cb96 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 @@ -1,5 +1,6 @@ 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.domain.common.ScanResponse @@ -12,9 +13,11 @@ 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.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.wallet.redux.Artwork +import com.tangem.tap.features.wallet.redux.WalletAction import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -84,6 +87,16 @@ private fun handleWalletAction(action: Action) { primaryCard = result.data.primaryCard ) onboardingManager.scanResponse = updatedResponse + val blockchainNetworks = listOf( + BlockchainNetwork(Blockchain.Bitcoin, result.data.card), + BlockchainNetwork(Blockchain.Ethereum, result.data.card) + ) + store.dispatchOnMain( + WalletAction.MultiWallet.SaveCurrencies( + blockchainNetworks = blockchainNetworks, + cardId = result.data.card.cardId + ) + ) onboardingManager.activationStarted(updatedResponse.card.cardId) store.dispatch(OnboardingWalletAction.ProceedBackup) } @@ -260,6 +273,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.proceedBackup { result -> when (result) { is CompletionResult.Success -> { + val blockchainNetworks = listOf( + BlockchainNetwork(Blockchain.Bitcoin, result.data), + BlockchainNetwork(Blockchain.Ethereum, result.data) + ) + store.dispatchOnMain( + WalletAction.MultiWallet.SaveCurrencies( + blockchainNetworks = blockchainNetworks, cardId = result.data.cardId + ) + ) if (backupService.currentState == BackupService.State.Finished) { store.dispatchOnMain(BackupAction.FinishBackup) } else { 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 41ccf7083b..b51a53e29f 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 @@ -20,7 +20,7 @@ class UnfinishedBackupFoundDialog { } setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> store.dispatch(GlobalAction.HideDialog) - store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup)) + store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup)) } setCancelable(false) }.create() diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 2ed9fe33e0..8c76739e6b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -4,6 +4,7 @@ import com.tangem.Message import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager +import com.tangem.common.core.TangemSdkError import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.ToastNotificationAction @@ -155,7 +156,10 @@ sealed class SendAction : SendScreenAction { val reduceAmount: BigDecimal, ) : Dialog() - data class SendTransactionFails(val errorMessage: String) : Dialog() + sealed class SendTransactionFails : Dialog() { + data class CardSdkError(val error: TangemSdkError): Dialog() + data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError): Dialog() + } object Hide : Dialog() } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 29165f8bad..cd4075a0ca 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -7,7 +7,7 @@ import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.demo.DemoTransactionSender -import com.tangem.tap.features.demo.isDemoWallet +import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.AmountActionUi import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.ReceiptAction @@ -28,6 +28,7 @@ class RequestFeeMiddleware { fun handle(appState: AppState?, dispatch: DispatchFunction) { val sendState = appState?.sendState ?: return val walletManager = sendState.walletManager ?: return + val scanResponse = appState.globalState.scanResponse ?: return if (!SendState.isReadyToRequestFee()) { dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY)) @@ -40,7 +41,7 @@ class RequestFeeMiddleware { val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!! val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto) - val txSender = if (walletManager.isDemoWallet()) { + val txSender = if (scanResponse.isDemoCard()) { DemoTransactionSender(walletManager) } else { walletManager as TransactionSender 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 1727c81944..df1e3dd5ba 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 @@ -4,7 +4,11 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.TransactionError +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -15,7 +19,12 @@ import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam -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.dispatchOnMain +import com.tangem.tap.common.extensions.logSendTransactionError +import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -26,9 +35,15 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.minimalAmount import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoTransactionSender -import com.tangem.tap.features.demo.isDemoWallet -import com.tangem.tap.features.send.redux.* +import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.features.send.redux.AddressPayIdActionUi +import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction +import com.tangem.tap.features.send.redux.AmountAction +import com.tangem.tap.features.send.redux.AmountActionUi import com.tangem.tap.features.send.redux.FeeAction.RequestFee +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.features.send.redux.SendActionUi import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.send.redux.states.ExternalTransactionData import com.tangem.tap.features.send.redux.states.MainCurrencyType @@ -44,7 +59,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.rekotlin.Action import org.rekotlin.Middleware -import timber.log.Timber import java.util.* /** @@ -65,13 +79,19 @@ class SendMiddleware { is SendActionUi.CheckIfTransactionDataWasProvided -> { val transactionData = appState()?.sendState?.externalTransactionData if (transactionData != null) { - store.dispatchOnMain(AddressPayIdVerifyAction.AddressVerification.SetWalletAddress( - transactionData.destinationAddress, false - )) + store.dispatchOnMain( + AddressPayIdVerifyAction.AddressVerification.SetWalletAddress( + transactionData.destinationAddress, false, + ), + ) store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO)) store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount)) - store.dispatchOnMain(AmountAction.SetAmount(transactionData.amount.toBigDecimal(), - false)) + store.dispatchOnMain( + AmountAction.SetAmount( + transactionData.amount.toBigDecimal(), + false, + ), + ) } } } @@ -79,7 +99,6 @@ class SendMiddleware { } } } - } private fun verifyAndSendTransaction( @@ -99,23 +118,31 @@ private fun verifyAndSendTransaction( when { hadTezosError -> { val reduceAmount = walletManager.wallet.blockchain.minimalAmount() - dispatch(SendAction.Dialog.TezosWarningDialog(reduceCallback = { - dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) - dispatch(AmountActionUi.CheckAmountToSend) - }, sendAllCallback = { - sendTransaction( - action, walletManager, amountToSend, feeAmount, destinationAddress, - sendState.transactionExtrasState, card, sendState.externalTransactionData, - dispatch - ) - }, reduceAmount)) + dispatch( + SendAction.Dialog.TezosWarningDialog( + reduceCallback = { + dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) + dispatch(AmountActionUi.CheckAmountToSend) + }, + sendAllCallback = { + sendTransaction( + action, walletManager, amountToSend, feeAmount, destinationAddress, + sendState.transactionExtrasState, card, sendState.externalTransactionData, + dispatch, + ) + }, + reduceAmount, + ), + ) } transactionErrors.isNotEmpty() -> { dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager))) } else -> { - sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress, - sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch) + sendTransaction( + action, walletManager, amountToSend, feeAmount, destinationAddress, + sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch, + ) } } } @@ -136,7 +163,9 @@ private fun sendTransaction( transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) } transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) } - transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it)) } + transactionExtras.xrpDestinationTag?.tag?.let { + txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it)) + } scope.launch { val updateWalletResult = walletManager.safeUpdate() @@ -161,17 +190,21 @@ private fun sendTransaction( tangemSdk.config.linkedTerminal = false } - val signer = TangemSigner(tangemSdk, action.messageForSigner) { signResponse -> + val signer = TangemSigner( + card = card, + tangemSdk = tangemSdk, + initialMessage = action.messageForSigner, + ) { signResponse -> store.dispatch( GlobalAction.UpdateWalletSignedHashes( walletSignedHashes = signResponse.totalSignedHashes, walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures - ) + remainingSignatures = signResponse.remainingSignatures, + ), ) } val sendResult = try { - if (walletManager.isDemoWallet()) { + if (card.isDemoCard()) { DemoTransactionSender(walletManager).send(txData, signer) } else { (walletManager as TransactionSender).send(txData, signer) @@ -186,14 +219,16 @@ private fun sendTransaction( return@launch } - withContext(Dispatchers.Main) { + withMainContext { + dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) tangemSdk.config.linkedTerminal = isLinkedTerminal + when (sendResult) { is SimpleResult.Success -> { store.state.globalState.analyticsHandlers?.triggerEvent( event = AnalyticsEvent.TRANSACTION_IS_SENT, card = card, - blockchain = walletManager.wallet.blockchain.currency + blockchain = walletManager.wallet.blockchain.currency, ) dispatch(SendAction.SendSuccess) @@ -213,76 +248,54 @@ private fun sendTransaction( } } is SimpleResult.Failure -> { - when (sendResult.error) { + store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( + wallet = walletManager.wallet, + host = walletManager.currentHost, + amountToSend = amountToSend, + feeAmount = feeAmount, + destinationAddress = destinationAddress, + ) + store.state.globalState.analyticsHandlers?.logSendTransactionError( + error = sendResult.error, + action = Analytics.ActionToLog.SendTransaction, + parameters = mapOf(AnalyticsParam.BLOCKCHAIN to walletManager.wallet.blockchain.currency), + card = card, + ) + + val error = (sendResult.error as? BlockchainSdkError) ?: return@withMainContext + + when (error) { + is BlockchainSdkError.WrappedTangemError -> { + val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return@withMainContext + if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext + + dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError)) + } is BlockchainSdkError.CreateAccountUnderfunded -> { - val error = sendResult.error as BlockchainSdkError.CreateAccountUnderfunded + // from XLM, XRP val reserve = error.minReserve.value?.stripZeroPlainString() ?: "0" val symbol = error.minReserve.currencySymbol dispatch(SendAction.SendError(TapError.CreateAccountUnderfunded(listOf(reserve, symbol)))) } - is BlockchainSdkError.SendException -> { - sendResult.error?.let { FirebaseCrashlytics.getInstance().recordException(it) } - } - is Throwable -> { - val throwable = sendResult.error as Throwable - val message = throwable.message - val infoHolder = store.state.globalState.feedbackManager?.infoHolder + else -> { when { - message == null -> { - dispatch(SendAction.SendError(TapError.UnknownError)) - infoHolder?.updateOnSendError( - wallet = walletManager.wallet, - host = walletManager.currentHost, - amountToSend = amountToSend, - feeAmount = feeAmount, - destinationAddress = destinationAddress + error.customMessage.contains(DemoTransactionSender.ID) -> { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_done, + messageId = R.string.alert_demo_feature_disabled, + onOk = { dispatch(NavigationAction.PopBackTo()) }, + ), ) - dispatch(SendAction.Dialog.SendTransactionFails("unknown error")) - } - message.contains("50002") -> { - // user was cancelled the operation by closing the Sdk bottom sheet - } - // make it easier latter by handling an appropriate enumError or, like on iOS, - // accept a string identifier of the error message - message.contains("Target account is not created. To create account send 1+ XLM.") -> { - dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) - } - message.contains(DemoTransactionSender.ID) -> { - delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchDialogShow(AppDialog.SimpleOkDialogRes( - R.string.common_done, - R.string.alert_demo_feature_disabled - ) { dispatch(NavigationAction.PopBackTo()) }) } else -> { - (sendResult.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( - error, - Analytics.ActionToLog.SendTransaction, - mapOf( - AnalyticsParam.BLOCKCHAIN - to walletManager.wallet.blockchain.currency), - card = card, - ) - } - Timber.e(throwable) - FirebaseCrashlytics.getInstance().recordException(throwable) - dispatch(SendAction.SendError(TapError.CustomError(message))) - infoHolder?.updateOnSendError( - wallet = walletManager.wallet, - host = walletManager.currentHost, - amountToSend = amountToSend, - feeAmount = feeAmount, - destinationAddress = destinationAddress - ) - dispatch(SendAction.Dialog.SendTransactionFails(message)) + dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) } } } } } } - dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) } } } @@ -310,7 +323,10 @@ fun extractErrorsForAmountField(errors: EnumSet): EnumSet, walletManager: WalletManager): TapError.ValidateTransactionErrors { +fun createValidateTransactionError( + errorList: EnumSet, + walletManager: WalletManager, +): TapError.ValidateTransactionErrors { val tapErrors = errorList.map { when (it) { TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance @@ -340,5 +356,4 @@ private fun updateWarnings(dispatch: (Action) -> Unit) { val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain)) dispatch(SendAction.Warnings.Set(warnings)) -} - +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index edb435953f..fd653557c7 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -106,8 +106,12 @@ class FeeReducer : SendInternalReducer { private fun getFeePrecision(sendState: SendState): FeePrecision { val blockchain = sendState.walletManager?.wallet?.blockchain - return if ((blockchain?.fullNameWithoutTestnet == Blockchain.Arbitrum.fullName || blockchain?.fullNameWithoutTestnet == Blockchain.Tron.fullName) && - sendState.amountState.typeOfAmount is AmountType.Token) { + return if ( + (blockchain?.fullNameWithoutTestnet == Blockchain.Arbitrum.fullName || + blockchain?.fullNameWithoutTestnet == Blockchain.Tron.fullName || + blockchain?.fullNameWithoutTestnet == Blockchain.Gnosis.fullName) && + sendState.amountState.typeOfAmount is AmountType.Token + ) { FeePrecision.CAN_BE_LOWER } else { FeePrecision.PRECISE diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 673f19cb66..07936d3e49 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -3,7 +3,18 @@ package com.tangem.tap.features.send.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.tap.common.CurrencyConverter import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.features.send.redux.* +import com.tangem.tap.features.send.redux.AddressPayIdActionUi +import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction +import com.tangem.tap.features.send.redux.AmountAction +import com.tangem.tap.features.send.redux.AmountActionUi +import com.tangem.tap.features.send.redux.FeeAction +import com.tangem.tap.features.send.redux.FeeActionUi +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.send.redux.ReceiptAction +import com.tangem.tap.features.send.redux.ReleaseSendState +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.features.send.redux.SendScreenAction +import com.tangem.tap.features.send.redux.TransactionExtrasAction import com.tangem.tap.features.send.redux.states.ExternalTransactionData import com.tangem.tap.features.send.redux.states.IdStateHolder import com.tangem.tap.features.send.redux.states.SendState @@ -46,7 +57,8 @@ private class SendReducer : SendInternalReducer { sendState.copy(sendButtonState = IndeterminateProgressButton(action.state)) } is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action) - is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action) + is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action) + is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action) is SendAction.Dialog.Hide -> sendState.copy(dialog = null) is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList) is SendAction.SendSpecificTransaction -> diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 8f4b106e97..a01c048058 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -2,8 +2,9 @@ package com.tangem.tap.features.send.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.tangem.tangem_sdk_new.extensions.localizedDescription +import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.feedback.SendTransactionFailedEmail import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.store import com.tangem.wallet.R @@ -14,14 +15,22 @@ import com.tangem.wallet.R class SendTransactionFailsDialog { companion object { - fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails): AlertDialog { + fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog { + return create(context, dialog.error.localizedDescription(context)) + } + + fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog { + return create(context, dialog.error.customMessage) + } + + private fun create(context: Context, errorMessage: String): AlertDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.alert_failed_to_send_transaction_title) - setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage)) + setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNeutralButton(R.string.alert_button_send_feedback) { _, _ -> - store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage))) + store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) } - setPositiveButton(R.string.common_no) { _, _ -> } + setPositiveButton(R.string.common_cancel) { _, _ -> } setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } }.create() } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 2b71c1ba31..4496bd570b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -8,7 +8,12 @@ import android.view.View import android.view.ViewGroup import androidx.core.text.bold import com.tangem.common.extensions.remove -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.enableError +import com.tangem.tap.common.extensions.getColor +import com.tangem.tap.common.extensions.getString +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.update import com.tangem.tap.common.redux.getMessageString import com.tangem.tap.common.text.DecimalDigitsInputFilter import com.tangem.tap.domain.MultiMessageError @@ -17,7 +22,17 @@ import com.tangem.tap.features.BaseStoreFragment import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.features.send.redux.states.* +import com.tangem.tap.features.send.redux.states.AddressPayIdState +import com.tangem.tap.features.send.redux.states.AmountState +import com.tangem.tap.features.send.redux.states.FeeState +import com.tangem.tap.features.send.redux.states.MainCurrencyType +import com.tangem.tap.features.send.redux.states.ReceiptLayoutType +import com.tangem.tap.features.send.redux.states.ReceiptState +import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.tap.features.send.redux.states.StateId +import com.tangem.tap.features.send.redux.states.TransactionExtraError +import com.tangem.tap.features.send.redux.states.TransactionExtrasState +import com.tangem.tap.features.send.redux.states.XlmMemoType import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog @@ -117,7 +132,13 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : dialog?.show() } } - is SendAction.Dialog.SendTransactionFails -> { + is SendAction.Dialog.SendTransactionFails.CardSdkError -> { + if (dialog == null) { + dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) + dialog?.show() + } + } + is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> { if (dialog == null) { dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) dialog?.show() diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt index 500cc39285..27a4a5b9e5 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -1,15 +1,20 @@ package com.tangem.tap.features.shop.redux import android.content.Intent +import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.shop.GooglePayService import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct +import com.tangem.wallet.R import org.rekotlin.Action sealed class ShopAction : Action { object LoadProducts : ShopAction() { data class Success(val products: List) : ShopAction() + object Failure : ShopAction(), NotificationAction { + override val messageResource = R.string.common_server_unavailable + } } data class ApplyPromoCode(val promoCode: String) : ShopAction() { diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt index 464a99de96..f3734950da 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt @@ -11,6 +11,7 @@ import com.tangem.tap.store import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware +import timber.log.Timber class ShopMiddleware { @@ -88,10 +89,13 @@ private fun handle(action: Action) { } ShopAction.LoadProducts -> { scope.launch { - val result = shopService.getProducts() - result.onSuccess { - store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) - } + shopService.getProducts().fold( + onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) }, + onFailure = { + Timber.e(it) + store.dispatchOnMain(ShopAction.LoadProducts.Failure) + } + ) } } is ShopAction.CheckIfGooglePayAvailable -> { diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt index ee12f53109..001c48a46f 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -65,5 +65,6 @@ private fun internalReduce(action: Action, state: ShopState): ShopState { } ShopAction.FinishSuccessfulOrder -> state ShopAction.ResetState -> ShopState() + ShopAction.LoadProducts.Failure -> state } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 7e1e4bee95..79f171b03f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -28,8 +28,6 @@ sealed class TokensAction : Action { val wallets: List, val derivationStyle: DerivationStyle? ) : TokensAction() - data class SetNonRemovableCurrencies(val wallets: List) : TokensAction() - data class SaveChanges( val addedTokens: List, val addedBlockchains: List 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 aab99a3d58..2ec703e629 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 @@ -30,7 +30,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.LoadAvailableCoinsService import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -44,13 +44,13 @@ class TokensMiddleware { when (action) { is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse) is TokensAction.SaveChanges -> handleSaveChanges(action) - is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken( - action - ) + is TokensAction.PrepareAndNavigateToAddCustomToken -> { + handleAddingCustomToken(action) + } is TokensAction.SetSearchInput -> { handleLoadCurrencies( scanResponse = store.state.globalState.scanResponse, - action.searchInput + newSearchInput = action.searchInput ) } is TokensAction.LoadMore -> { @@ -138,7 +138,9 @@ class TokensMiddleware { ) ) - if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) { + if (tokensToAdd.isEmpty() && tokensToRemove.isEmpty() + && blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() + ) { store.dispatchDebugErrorNotification("Nothing to save") store.dispatch(NavigationAction.PopBackTo()) return @@ -308,9 +310,10 @@ class TokensMiddleware { private fun removeCurrenciesIfNeeded(currencies: List) { if (currencies.isNotEmpty()) { currencies.forEach { currency -> - store.state.walletState.getWalletData(currency)?.let { - store.dispatch(WalletAction.MultiWallet.RemoveWallet(it)) - } + store.dispatch(WalletAction.MultiWallet.RemoveWallet( + currency = currency, + fromScreen = AppScreen.AddTokens + )) } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt index 374f6ad820..007a1e9711 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -43,13 +43,6 @@ private fun internalReduce(action: Action, state: AppState): TokensState { derivationStyle = action.derivationStyle ) } - is TokensAction.SetNonRemovableCurrencies -> { - tokensState.copy( - nonRemovableBlockchains = action.wallets.toNonCustomBlockchains(tokensState.derivationStyle), - nonRemovableTokens = action.wallets.toTokensContractAddresses(), - ) - } - is TokensAction.AllowToAddTokens -> { tokensState.copy(allowToAdd = action.allow) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index 1c60655d24..ab1b3731a5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -14,8 +14,6 @@ data class TokensState( val addedWallets: List = emptyList(), val addedTokens: List = emptyList(), val addedBlockchains: List = emptyList(), - val nonRemovableTokens: List = emptyList(), - val nonRemovableBlockchains: List = emptyList(), val currencies: List = emptyList(), val searchInput: String? = null, val allowToAdd: Boolean = true, @@ -34,18 +32,18 @@ data class TokensState( typealias ContractAddress = String fun List.toTokensContractAddresses(): List { - return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token?.contractAddress }.distinct() + return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.models.Currency.Token)?.token?.contractAddress }.distinct() } fun List.toNonCustomTokens(derivationStyle: DerivationStyle?): List { return filter { !it.currency.isCustomCurrency(derivationStyle) } - .mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token } + .mapNotNull { (it.currency as? com.tangem.tap.features.wallet.models.Currency.Token)?.token } .distinct() } fun List.toNonCustomTokensWithBlockchains(derivationStyle: DerivationStyle?): List { return mapNotNull { - if (it.currency !is com.tangem.tap.features.wallet.redux.Currency.Token) return@mapNotNull null + if (it.currency !is com.tangem.tap.features.wallet.models.Currency.Token) return@mapNotNull null if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null TokenWithBlockchain(it.currency.token, it.currency.blockchain) }.distinct() @@ -56,7 +54,7 @@ fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): if (it.currency.isCustomCurrency(derivationStyle)) { null } else { - (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Blockchain)?.blockchain + (it.currency as? com.tangem.tap.features.wallet.models.Currency.Blockchain)?.blockchain } }.distinct() } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt deleted file mode 100644 index 5ac13c1815..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.tangem.tap.features.tokens.ui.compose - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.tap.common.extensions.getGreyedOutIconRes -import com.tangem.tap.common.extensions.getRoundIconRes -import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.features.tokens.redux.TokenWithBlockchain -import com.tangem.wallet.R - -@Composable -fun CollapsedCurrencyItem( - currency: Currency, - addedTokens: List, - addedBlockchains: List, - onCurrencyClick: (String) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { onCurrencyClick(currency.id) }) - ) { - val blockchain = Blockchain.fromNetworkId(currency.id) - - SubcomposeAsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(currency.iconUrl) - .crossfade(true) - .build(), - contentDescription = currency.id, - loading = { CurrencyPlaceholderIcon(currency.id) }, - error = { CurrencyPlaceholderIcon(currency.id) }, - modifier = Modifier - .clip(CircleShape) - .padding(16.dp) - .size(46.dp), - ) - Column( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - .align(Alignment.CenterVertically) - ) { - Text( - text = currency.fullName, - fontSize = 17.sp, - fontWeight = FontWeight.Normal, - color = Color(0xFF1C1C1E), - ) - Spacer(modifier = Modifier.size(6.dp)) - Row { - if (!currency.contracts.isNullOrEmpty()) { - currency.contracts.map { contract -> - if (contract.address == null) { - BlockchainNetworkItem( - blockchain = contract.blockchain, - isMainNetwork = true, - addedBlockchains = addedBlockchains - ) - } else { - val added = - addedTokens.map { it.token.contractAddress } - .contains(contract.address) - val icon = if (added) { - contract.blockchain.getRoundIconRes() - } else { - contract.blockchain.getGreyedOutIconRes() - } - Image( - painter = painterResource(id = icon), - contentDescription = null, - Modifier - .size(20.dp) - ) - Spacer(modifier = Modifier.size(5.dp)) - } - - } - } else { - BlockchainNetworkItem( - blockchain = blockchain, - isMainNetwork = false, - addedBlockchains = addedBlockchains - ) - } - } - } - Image( - painter = painterResource(id = R.drawable.ic_arrow_collapsed), - contentDescription = null, - Modifier - .padding(20.dp) - .align(Alignment.CenterVertically) - - ) - } -} - -@Composable -fun BlockchainNetworkItem( - blockchain: Blockchain?, - isMainNetwork: Boolean, - addedBlockchains: List -) { - val added = addedBlockchains.contains(blockchain) - val icon = - if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes() - if (icon != null) { - Box(Modifier - .size(20.dp)) { - Image( - painter = painterResource(id = icon), - contentDescription = null, - Modifier - .size(20.dp) - ) - if (isMainNetwork) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(7.dp) - .clip(CircleShape) - .background(Color.White) - ) { - Box( - modifier = Modifier - .align(Alignment.Center) - .size(5.dp) - .clip(CircleShape) - .background(Color(0xFF1ACE80)) - ) - } - } - } - Spacer(modifier = Modifier.size(5.dp)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index ef87846c21..49886cbf86 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -21,6 +21,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.compose.Keyboard +import com.tangem.tap.common.compose.extensions.addAndNotify +import com.tangem.tap.common.compose.extensions.removeAndNotify import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.pixelsToDp @@ -30,6 +32,7 @@ import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.LoadCoinsState import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.tap.features.tokens.redux.TokensState +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -40,30 +43,30 @@ fun CurrenciesScreen( onNetworkItemClicked: (ContractAddress) -> Unit, onLoadMore: () -> Unit ) { - val context = LocalContext.current + val tokensAddedOnMainScreen = remember { tokensState.value.addedTokens } + val blockchainsAddedOnMainScreen = remember { tokensState.value.addedBlockchains } + val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) } val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) } val isKeyboardOpen by keyboardAsState() val onAddCurrencyToggleClick = { currency: Currency, token: TokenWithBlockchain? -> - if (token != null) { - val mutableList = addedTokensState.value.toMutableList() - if (mutableList.contains(token)) { - mutableList.remove(token) - } else { - mutableList.add(token) - } - addedTokensState.value = mutableList - } else { - val blockchain = Blockchain.fromNetworkId(currency.id) - val mutableList = addedBlockchainsState.value.toMutableList() - if (mutableList.contains(blockchain)) { - mutableList.remove(blockchain) - } else { - blockchain?.let { mutableList.add(blockchain) } - } - addedBlockchainsState.value = mutableList + val blockchain = Blockchain.fromNetworkId(currency.id) + if (blockchain != null && token == null) { + toggleBlockchain( + blockchain, + blockchainsAddedOnMainScreen, + addedBlockchainsState, + addedTokensState.value, + ) + } else if (token != null) { + toggleToken( + token, + tokensAddedOnMainScreen, + addedTokensState, + tokensState.value, + ) } } @@ -100,23 +103,11 @@ fun CurrenciesScreen( ListOfCurrencies( header = { if (showHeader) CurrenciesWarning() }, currencies = tokensState.value.currencies, - nonRemovableTokens = tokensState.value.nonRemovableTokens, - nonRemovableBlockchains = tokensState.value.nonRemovableBlockchains, addedTokens = addedTokensState.value, addedBlockchains = addedBlockchainsState.value, allowToAdd = tokensState.value.allowToAdd, onAddCurrencyToggled = { currency, token -> onAddCurrencyToggleClick(currency, token) - token?.let { - if (!tokensState.value.canHandleToken(it)) { - val dialog = AppDialog.SimpleOkDialog( - header = context.getString(R.string.common_warning), - message = context.getString(R.string.alert_manage_tokens_unsupported_message) - ) { onAddCurrencyToggleClick(currency, it) } - store.dispatchDialogShow(dialog) - } - } - }, onNetworkItemClicked = onNetworkItemClicked, onLoadMore = onLoadMore @@ -127,6 +118,68 @@ fun CurrenciesScreen( } } +private fun toggleBlockchain( + blockchain: Blockchain, + blockchainsAddedOnMainScreen: List, + addedBlockchainsState: MutableState>, + addedTokens: List +) { + val isTryingToRemove = addedBlockchainsState.value.contains(blockchain) + val isAddedOnMainScreen = blockchainsAddedOnMainScreen.contains(blockchain) + val isTokenWithSameBlockchainFound = addedTokens.any { it.blockchain == blockchain } + + if (isTryingToRemove) { + if (isTokenWithSameBlockchainFound) { + store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog( + currencyTitle = blockchain.name, + currencySymbol = blockchain.currency + )) + } else { + if (isAddedOnMainScreen) { + store.dispatchDialogShow(WalletDialog.RemoveWalletDialog( + currencyTitle = blockchain.name, + onOk = { addedBlockchainsState.removeAndNotify(blockchain) } + )) + } else { + addedBlockchainsState.removeAndNotify(blockchain) + } + } + } else { + addedBlockchainsState.addAndNotify(blockchain) + } +} + +private fun toggleToken( + token: TokenWithBlockchain, + tokensAddedOnMainScreen: List, + addedTokensState: MutableState>, + tokensState: TokensState, +) { + val isTryingToRemove = addedTokensState.value.contains(token) + val isAddedOnMainScreen = tokensAddedOnMainScreen.contains(token) + val isUnsupportedToken = !tokensState.canHandleToken(token) + + if (isTryingToRemove) { + if (isAddedOnMainScreen) { + store.dispatchDialogShow(WalletDialog.RemoveWalletDialog( + currencyTitle = token.token.name, + onOk = { addedTokensState.removeAndNotify(token) } + )) + } else { + addedTokensState.removeAndNotify(token) + } + } else { + if (isUnsupportedToken) { + store.dispatchDialogShow(AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.alert_manage_tokens_unsupported_message, + )) + } else { + addedTokensState.addAndNotify(token) + } + } +} + @Composable fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt index ef8c92c104..e69de29bb2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt @@ -1,159 +0,0 @@ -package com.tangem.tap.features.tokens.ui.compose - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.features.tokens.redux.ContractAddress -import com.tangem.tap.features.tokens.redux.TokenWithBlockchain -import com.tangem.wallet.R - -@Composable -fun ExpandedCurrencyItem( - currency: Currency, - nonRemovableTokens: List, - nonRemovableBlockchains: List, - addedTokens: List, - addedBlockchains: List, - allowToAdd: Boolean, - onCurrencyClick: (String) -> Unit, - onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit, - onNetworkItemClicked: (ContractAddress) -> Unit -) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { onCurrencyClick(currency.id) }) - ) { - SubcomposeAsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(currency.iconUrl) - .crossfade(true) - .build(), - contentDescription = currency.id, - loading = { CurrencyPlaceholderIcon(currency.id) }, - error = { CurrencyPlaceholderIcon(currency.id) }, - modifier = Modifier - .clip(CircleShape) - .padding(start = 16.dp, top = 16.dp, end = 16.dp) - .size(46.dp), - ) - Column( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - .padding(top = 14.dp) - .align(Alignment.CenterVertically) - ) { - Text( - text = currency.fullName, - fontSize = 17.sp, - fontWeight = FontWeight.Normal, - color = Color(0xFF1C1C1E), - ) - Spacer(modifier = Modifier.size(6.dp)) - Text( - text = stringResource(id = R.string.currency_subtitle_expanded), - fontSize = 13.sp, - fontWeight = FontWeight.Normal, - color = Color(0xFF8E8E93), - ) - } - Image( - painter = painterResource(id = R.drawable.ic_arrow_extended), - contentDescription = null, - Modifier - .padding(20.dp) - .align(Alignment.CenterVertically) - ) - } - - val blockchains = currency.contracts.map { it.blockchain } - - Row { - Box( - modifier = Modifier - .fillMaxHeight() - ) { - if (blockchains.size > 1) { - val dividerHeight = if (blockchains.size == 2) { - 60 - } else if (blockchains.size == 3) { - 110 - } else { - 43 * blockchains.size - } - Divider( - color = Color(0xFFDEDEDE), - modifier = Modifier - .height(dividerHeight.dp) - .padding(start = 37.5.dp) - .width(1.dp) - ) - } - - Column { - blockchains.map { - Image( - painter = painterResource(id = R.drawable.ic_link), - contentDescription = null, - Modifier - .padding( - start = 37.dp, - ) - ) - Spacer(modifier = Modifier.size(13.5.dp)) - } - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 6.dp), - ) { - blockchains.map { blockchain -> - val contract = currency.contracts.firstOrNull { it.blockchain == blockchain } - ?: return@map - val added = if (contract.address != null) { - addedTokens.map { it.token.contractAddress }.contains(contract.address) - } else { - addedBlockchains.contains(blockchain) - } - val canBeRemoved = if (contract.address != null) { - !nonRemovableTokens.contains(contract.address) - } else { - !nonRemovableBlockchains.contains(blockchain) - } - NetworkItem( - currency = currency, - contract = contract, - blockchain = blockchain, allowToAdd = allowToAdd, - added = added, - canBeRemoved = canBeRemoved, - onAddCurrencyToggled = onAddCurrencyToggled, - onNetworkItemClicked = onNetworkItemClicked, - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt index 4892cf5e13..613f741c55 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.ui.compose +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn @@ -16,20 +17,18 @@ import com.tangem.tap.common.compose.extensions.OnBottomReached import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain - +import com.tangem.tap.features.tokens.ui.compose.currencyItem.CurrencyItem @Composable fun ListOfCurrencies( header: @Composable () -> Unit, currencies: List, - nonRemovableTokens: List, - nonRemovableBlockchains: List, addedTokens: List, addedBlockchains: List, allowToAdd: Boolean, onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit, onNetworkItemClicked: (ContractAddress) -> Unit, - onLoadMore: () -> Unit + onLoadMore: () -> Unit, ) { val expandedCurrencies = remember { mutableStateOf(listOf("")) } @@ -52,21 +51,20 @@ fun ListOfCurrencies( state = listState, modifier = Modifier .fillMaxSize(), - contentPadding = PaddingValues(bottom = 90.dp) + contentPadding = PaddingValues(bottom = 90.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { item { header() } itemsIndexed(currencies) { index, currency -> CurrencyItem( currency = currency, - nonRemovableTokens = nonRemovableTokens, - nonRemovableBlockchains = nonRemovableBlockchains, addedTokens = addedTokens, addedBlockchains = addedBlockchains, allowToAdd = allowToAdd, - expanded = expandedCurrencies.value.contains(currency.id), + isExpanded = expandedCurrencies.value.contains(currency.id), onCurrencyClick = onCurrencyClick, onAddCurrencyToggled = onAddCurrencyToggled, - onNetworkItemClicked = onNetworkItemClicked + onNetworkItemClicked = onNetworkItemClicked, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyExpandedContent.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyExpandedContent.kt new file mode 100644 index 0000000000..cded760ba9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyExpandedContent.kt @@ -0,0 +1,60 @@ +package com.tangem.tap.features.tokens.ui.compose.currencyItem + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.domain.tokens.Currency +import com.tangem.tap.features.tokens.redux.ContractAddress +import com.tangem.tap.features.tokens.redux.TokenWithBlockchain + +@Composable +fun CurrencyExpandedContent( + currency: Currency, + addedTokens: List, + addedBlockchains: List, + allowToAdd: Boolean, + isExpanded: Boolean, + onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit, + onNetworkItemClicked: (ContractAddress) -> Unit, +) { + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + val blockchains = currency.contracts.map { it.blockchain } + Column( + modifier = Modifier + .fillMaxWidth(), + ) { + blockchains.mapIndexed { index, blockchain -> + val contract = currency.contracts.firstOrNull { it.blockchain == blockchain } + ?: return@mapIndexed + + val added = if (contract.address != null) { + addedTokens.map { it.token.contractAddress }.contains(contract.address) + } else { + addedBlockchains.contains(blockchain) + } + NetworkItem( + currency = currency, + contract = contract, + blockchain = blockchain, + allowToAdd = allowToAdd, + added = added, + index = index, + size = blockchains.size, + onAddCurrencyToggled = onAddCurrencyToggled, + onNetworkItemClicked = onNetworkItemClicked, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItem.kt similarity index 50% rename from app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt rename to app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItem.kt index 9fd962bce7..6758c71e82 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItem.kt @@ -1,5 +1,6 @@ -package com.tangem.tap.features.tokens.ui.compose +package com.tangem.tap.features.tokens.ui.compose.currencyItem +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import com.tangem.blockchain.common.Blockchain import com.tangem.tap.domain.tokens.Currency @@ -9,31 +10,30 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain @Composable fun CurrencyItem( currency: Currency, - nonRemovableTokens: List, - nonRemovableBlockchains: List, addedTokens: List, addedBlockchains: List, allowToAdd: Boolean, - expanded: Boolean, + isExpanded: Boolean, onCurrencyClick: (String) -> Unit, onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit, - onNetworkItemClicked: (ContractAddress) -> Unit + onNetworkItemClicked: (ContractAddress) -> Unit, ) { - if (expanded) { - ExpandedCurrencyItem( + Column { + CurrencyItemHeader( + currency = currency, + addedTokens = addedTokens, + addedBlockchains = addedBlockchains, + isExpanded = isExpanded, + onCurrencyClick = onCurrencyClick, + ) + CurrencyExpandedContent( currency = currency, - nonRemovableTokens = nonRemovableTokens, - nonRemovableBlockchains = nonRemovableBlockchains, addedTokens = addedTokens, addedBlockchains = addedBlockchains, allowToAdd = allowToAdd, - onCurrencyClick = onCurrencyClick, onAddCurrencyToggled = onAddCurrencyToggled, - onNetworkItemClicked = onNetworkItemClicked - ) - } else { - CollapsedCurrencyItem( - currency = currency, addedTokens = addedTokens, - addedBlockchains = addedBlockchains, onCurrencyClick + isExpanded = isExpanded, + onAddCurrencyToggled = onAddCurrencyToggled, + onNetworkItemClicked = onNetworkItemClicked, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt new file mode 100644 index 0000000000..4be02043d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt @@ -0,0 +1,91 @@ +package com.tangem.tap.features.tokens.ui.compose.currencyItem + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun CurrencyItemArrow( + rowHeight: Dp, + isLastArrow: Boolean, +) { + Box(Modifier.height(rowHeight)) { + if (!isLastArrow) { + MiddleArrowView(rowHeight) + } else { + LastArrowView(rowHeight) + } + } +} + +@Composable +private fun MiddleArrowView(rowHeight: Dp) { + Box { + Box( + modifier = Modifier + .padding(start = 0.5.dp), + ) { + Box( + modifier = Modifier + .background(color = Color(0xFFDEDEDE)) + .width(0.8.dp) + .fillMaxHeight(), + ) + } + + LastArrowView(rowHeight) + } +} + +@Composable +private fun LastArrowView(rowHeight: Dp) { + Image( + modifier = Modifier + .height(rowHeight / 2 + 3.5.dp), + painter = painterResource(id = R.drawable.ic_link), + contentDescription = null, + ) +} + +@Preview +@Composable +fun ArrowViewPreview() { + Box( + Modifier + .background(color = Color.White), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 37.dp), + ) { + CurrencyItemArrow( + rowHeight = 62.dp, + isLastArrow = false, + ) + CurrencyItemArrow( + rowHeight = 62.dp, + isLastArrow = true, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemHeader.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemHeader.kt new file mode 100644 index 0000000000..f3d40a401b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemHeader.kt @@ -0,0 +1,208 @@ +package com.tangem.tap.features.tokens.ui.compose.currencyItem + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.tap.common.extensions.getGreyedOutIconRes +import com.tangem.tap.common.extensions.getRoundIconRes +import com.tangem.tap.domain.tokens.Currency +import com.tangem.tap.features.tokens.redux.TokenWithBlockchain +import com.tangem.tap.features.tokens.ui.compose.CurrencyPlaceholderIcon +import com.tangem.tap.features.tokens.ui.compose.fullName +import com.tangem.wallet.R + +@Composable +fun CurrencyItemHeader( + currency: Currency, + addedTokens: List, + addedBlockchains: List, + isExpanded: Boolean, + onCurrencyClick: (String) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(62.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onCurrencyClick(currency.id) }, + ), + ) { + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(6.dp)), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(46.dp), + model = ImageRequest.Builder(LocalContext.current) + .data(currency.iconUrl) + .crossfade(true) + .build(), + contentDescription = currency.id, + loading = { CurrencyPlaceholderIcon(currency.id) }, + error = { CurrencyPlaceholderIcon(currency.id) }, + ) + } + + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), + ) { + Text( + text = currency.fullName, + fontSize = 17.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF1C1C1E), + ) + Spacer(modifier = Modifier.size(6.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + ) { + if (isExpanded) { + Text( + modifier = Modifier.align(Alignment.TopStart), + text = stringResource(id = R.string.currency_subtitle_expanded), + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF8E8E93), + ) + } else { + NetworksRow( + currency = currency, + addedTokens = addedTokens, + addedBlockchains = addedBlockchains, + ) + } + } + } + + val expandCollapseArrow = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed + Image( + painter = painterResource(id = expandCollapseArrow), + contentDescription = null, + Modifier + .padding(20.dp) + .align(Alignment.CenterVertically), + ) + } +} + +@Composable +private fun NetworksRow( + currency: Currency, + addedTokens: List, + addedBlockchains: List, +) { + Row { + if (currency.contracts.isNotEmpty()) { + currency.contracts.map { contract -> + if (contract.address == null) { + BlockchainNetworkItem( + blockchain = contract.blockchain, + isMainNetwork = true, + addedBlockchains = addedBlockchains, + ) + } else { + val isAdded = addedTokens.map { it.token.contractAddress }.contains(contract.address) + val iconResId = if (isAdded) { + contract.blockchain.getRoundIconRes() + } else { + contract.blockchain.getGreyedOutIconRes() + } + Image( + modifier = Modifier + .size(20.dp), + painter = painterResource(id = iconResId), + contentDescription = null, + ) + Spacer(Modifier.size(5.dp)) + } + + } + } else { + BlockchainNetworkItem( + blockchain = Blockchain.fromNetworkId(currency.id), + isMainNetwork = false, + addedBlockchains = addedBlockchains, + ) + } + } +} + +@Composable +private fun BlockchainNetworkItem( + blockchain: Blockchain?, + isMainNetwork: Boolean, + addedBlockchains: List, +) { + val added = addedBlockchains.contains(blockchain) + val icon = + if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes() + if (icon != null) { + Box( + Modifier + .size(20.dp), + ) { + Image( + painter = painterResource(id = icon), + contentDescription = null, + Modifier + .size(20.dp), + ) + if (isMainNetwork) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(7.dp) + .clip(CircleShape) + .background(Color.White), + ) { + Box( + modifier = Modifier + .align(Alignment.Center) + .size(5.dp) + .clip(CircleShape) + .background(Color(0xFF1ACE80)), + ) + } + } + } + Spacer(modifier = Modifier.size(5.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt similarity index 68% rename from app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt rename to app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt index 9b75a86359..2928ace0d8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt @@ -1,10 +1,18 @@ -package com.tangem.tap.features.tokens.ui.compose +package com.tangem.tap.features.tokens.ui.compose.currencyItem import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +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.shape.CircleShape import androidx.compose.material.Switch import androidx.compose.material.SwitchDefaults @@ -31,20 +39,27 @@ import com.tangem.tap.domain.tokens.Contract import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain +import com.tangem.tap.features.tokens.ui.compose.CurrencyPlaceholderIcon @OptIn(ExperimentalFoundationApi::class) @Composable fun NetworkItem( - currency: Currency, contract: Contract, - blockchain: Blockchain, allowToAdd: Boolean, - added: Boolean, canBeRemoved: Boolean, + currency: Currency, + contract: Contract, + blockchain: Blockchain, + allowToAdd: Boolean, + added: Boolean, + index: Int, + size: Int, onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit, - onNetworkItemClicked: (ContractAddress) -> Unit + onNetworkItemClicked: (ContractAddress) -> Unit, ) { + val rowHeight = 53.dp Row( modifier = Modifier .fillMaxWidth() + .height(rowHeight) .combinedClickable( enabled = allowToAdd, onLongClick = { @@ -52,13 +67,22 @@ fun NetworkItem( }, onClick = {}, indication = null, - interactionSource = remember { MutableInteractionSource() } - ) + interactionSource = remember { MutableInteractionSource() }, + ), ) { + Box( + Modifier + .width(78.dp) + .padding(start = 38.dp), + ) { + CurrencyItemArrow( + rowHeight = rowHeight, + isLastArrow = index == size - 1, + ) + } Box( modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp) + .align(Alignment.CenterVertically), ) { SubcomposeAsyncImage( model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(), @@ -66,7 +90,7 @@ fun NetworkItem( loading = { CurrencyPlaceholderIcon(blockchain.id) }, error = { CurrencyPlaceholderIcon(blockchain.id) }, modifier = Modifier - .size(20.dp) + .size(20.dp), ) if (contract.address == null) { Box( @@ -74,34 +98,31 @@ fun NetworkItem( .align(Alignment.TopEnd) .size(7.dp) .clip(CircleShape) - .background(Color.White) + .background(Color.White), ) { Box( modifier = Modifier .align(Alignment.Center) .size(5.dp) .clip(CircleShape) - .background(Color(0xFF1ACE80)) + .background(Color(0xFF1ACE80)), ) } } } - Row( + Spacer(Modifier.width(6.dp)) + Text( modifier = Modifier - .fillMaxHeight() .weight(1f) - .align(Alignment.CenterVertically) - ) { - Text( - text = prepareNetworkNameSpannableText( - blockchain = blockchain, - contractAddress = contract.address - ), - fontWeight = FontWeight.SemiBold, - fontSize = 13.sp, - color = if (added) Color.Black else Color(0xFF848488), - ) - } + .align(Alignment.CenterVertically), + text = prepareNetworkNameSpannableText( + blockchain = blockchain, + contractAddress = contract.address, + ), + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + color = if (added) Color.Black else Color(0xFF848488), + ) if (allowToAdd) { val token = if (contract.address != null) { @@ -115,8 +136,7 @@ fun NetworkItem( } else { null } - val tokenWithBlockchain = - token?.let { TokenWithBlockchain(token, contract.blockchain) } + val tokenWithBlockchain = token?.let { TokenWithBlockchain(it, contract.blockchain) } val currencyToSave = if (contract.address == null) { currency.copy(id = contract.networkId) @@ -125,23 +145,23 @@ fun NetworkItem( } Switch( + modifier = Modifier + .fillMaxHeight() + .padding(start = 16.dp, end = 16.dp), checked = added, - enabled = canBeRemoved, onCheckedChange = { onAddCurrencyToggled(currencyToSave, tokenWithBlockchain) }, - modifier = Modifier.padding(start = 16.dp, end = 16.dp), colors = SwitchDefaults.colors( - checkedThumbColor = Color(0xFF1ACE80) - ) + checkedThumbColor = Color(0xFF1ACE80), + ), ) } } } - @Composable -fun prepareNetworkNameSpannableText( +private fun prepareNetworkNameSpannableText( blockchain: Blockchain, - contractAddress: String? + contractAddress: String?, ): AnnotatedString { val blockchainName = blockchain.fullNameWithoutTestnet.uppercase() @@ -157,11 +177,11 @@ fun prepareNetworkNameSpannableText( AnnotatedString.Range( SpanStyle( fontWeight = FontWeight.Normal, - color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80) + color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80), ), start = startOfAdditionalText, - end = text.length - ) + end = text.length, + ), ) return AnnotatedString(text = text, spanStyles = spanStyles) } \ No newline at end of file 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 new file mode 100644 index 0000000000..75f752b278 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -0,0 +1,95 @@ +package com.tangem.tap.features.wallet.models + +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.features.addCustomToken.CustomCurrency +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.tokens.redux.TokenWithBlockchain + +sealed interface Currency { + + val coinId: String? + get() = when (this) { + is Blockchain -> blockchain.toCoinId() + is Token -> token.id + } + val blockchain: com.tangem.blockchain.common.Blockchain + val currencySymbol: CryptoCurrencyName + val derivationPath: String? + + val currencyName: String + get() = when (this) { + is Blockchain -> blockchain.fullName + is Token -> token.name + } + + data class Token( + val token: com.tangem.blockchain.common.Token, + override val blockchain: com.tangem.blockchain.common.Blockchain, + override val derivationPath: String? + ) : Currency { + override val currencySymbol = token.symbol + } + + data class Blockchain( + override val blockchain: com.tangem.blockchain.common.Blockchain, + override val derivationPath: String? + ) : Currency { + override val currencySymbol: CryptoCurrencyName = blockchain.currency + } + + fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { + if (this is Token && this.token.id == null) return true + + if (derivationPath == null || derivationStyle == null) return false + + return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath + } + + fun isBlockchain(): Boolean = this is Blockchain + + fun isToken(): Boolean = this is Token + + companion object { + fun fromBlockchainNetwork( + blockchainNetwork: BlockchainNetwork, + token: com.tangem.blockchain.common.Token? = null + ): Currency { + return if (token != null) { + Token( + token = token, + blockchain = blockchainNetwork.blockchain, + derivationPath = blockchainNetwork.derivationPath + ) + } else { + Blockchain( + blockchain = blockchainNetwork.blockchain, + derivationPath = blockchainNetwork.derivationPath + ) + } + } + + fun fromCustomCurrency(customCurrency: CustomCurrency): Currency { + return when (customCurrency) { + is CustomCurrency.CustomBlockchain -> Blockchain( + blockchain = customCurrency.network, + derivationPath = customCurrency.derivationPath?.rawPath + ) + is CustomCurrency.CustomToken -> Token( + token = customCurrency.token, + blockchain = customCurrency.network, + derivationPath = customCurrency.derivationPath?.rawPath, + ) + } + } + + fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token { + return Token( + token = tokenWithBlockchain.token, + blockchain = tokenWithBlockchain.blockchain, + derivationPath = null + ) + } + } +} \ No newline at end of file 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 c90af2af53..e079a6b114 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 @@ -11,16 +11,18 @@ import com.tangem.common.card.Card import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.models.Currency import com.tangem.wallet.R -import java.math.BigDecimal import org.rekotlin.Action +import java.math.BigDecimal sealed class WalletAction : Action { - object ResetState : WalletAction() + data class ResetState(val newCardId: String) : WalletAction() data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction() @@ -63,7 +65,9 @@ sealed class WalletAction : Action { MultiWallet() data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet() - data class SaveCurrencies(val blockchainNetworks: List) : MultiWallet() + data class SaveCurrencies( + val blockchainNetworks: List, val cardId: String? = null + ) : MultiWallet() // object FindTokensInUse : MultiWallet() // object FindBlockchainsInUse : MultiWallet() @@ -74,9 +78,18 @@ sealed class WalletAction : Action { ) : MultiWallet() data class SelectWallet(val walletData: WalletData?) : MultiWallet() - data class RemoveWallet(val walletData: WalletData) : MultiWallet() + + data class TryToRemoveWallet(val currency: Currency) : MultiWallet() + data class RemoveWallet( + val currency: Currency, + val fromScreen: AppScreen + ) : MultiWallet() + data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet() data class SetPrimaryToken(val token: Token) : MultiWallet() + + data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet() + object BackupWallet : MultiWallet() } sealed class Warnings : WalletAction() { @@ -97,8 +110,6 @@ sealed class WalletAction : Action { } class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings() - - object RestoreFundsWarningClosed : Warnings() } data class LoadFiatRate( 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 749f84b926..48c886d939 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 @@ -1,26 +1,20 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.common.extensions.isZero -import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.entities.Button -import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.toQrCode -import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState -import com.tangem.tap.features.tokens.redux.TokenWithBlockchain +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.models.TotalBalance import com.tangem.tap.features.wallet.models.WalletRent @@ -32,13 +26,13 @@ import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount import com.tangem.tap.features.wallet.redux.reducers.findProgressState import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.store +import org.rekotlin.StateType import java.math.BigDecimal import kotlin.properties.ReadOnlyProperty -import org.rekotlin.StateType data class WalletState( + val cardId: String = "", val state: ProgressState = ProgressState.Done, val error: ErrorType? = null, val cardImage: Artwork? = null, @@ -52,25 +46,20 @@ data class WalletState( val primaryToken: Token? = null, val isTestnet: Boolean = false, val totalBalance: TotalBalance? = null, + val showBackupWarning: Boolean = false, ) : StateType { // if you do not delegate - the application crashes on startup, // because twinCardsState has not been created yet - val twinCardsState: TwinCardsState by ReadOnlyProperty { thisRef, property -> + val twinCardsState: TwinCardsState by ReadOnlyProperty { _, _ -> store.state.twinCardsState } val isTangemTwins: Boolean get() = store.state.globalState.scanResponse?.isTangemTwins() == true - val primaryWallet: WalletData? = wallets.firstOrNull() - ?.walletsData?.firstOrNull() - val primaryWalletManager: WalletManager? = - if (wallets.isNotEmpty()) wallets[0].walletManager else null - - val shouldShowDetails: Boolean = - primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && - primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain + val isExchangeServiceFeatureOn: Boolean + get() = store.state.globalState.exchangeManager.featureIsSwitchedOn() val blockchains: List get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain } @@ -84,6 +73,14 @@ data class WalletState( val walletManagers: List get() = wallets.mapNotNull { it.walletManager } + val primaryWallet: WalletData? = wallets.firstOrNull()?.walletsData?.firstOrNull() + + val primaryWalletManager: WalletManager? = if (wallets.isNotEmpty()) wallets[0].walletManager else null + + val shouldShowDetails: Boolean = + primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && + primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain + fun getWalletManager(currency: Currency?): WalletManager? { if (currency?.blockchain == null) return null return getWalletStore(currency)?.walletManager @@ -256,32 +253,6 @@ data class WalletState( return updatedWallets + remainingWallets } - fun updateTradeCryptoState( - exchangeManager: CurrencyExchangeManager?, - walletData: WalletData - ): WalletData { - return walletData.copy( - tradeCryptoState = TradeCryptoState.from( - exchangeManager, - walletData - ) - ) - } - - fun updateTradeCryptoState( - exchangeManager: CurrencyExchangeManager?, - walletDataList: List - ): List { - return walletDataList.map { - it.copy( - tradeCryptoState = TradeCryptoState.from( - exchangeManager, - it - ) - ) - } - } - private fun updateTotalBalance(): WalletState { val walletsData = this.wallets .flatMap(WalletStore::walletsData) @@ -319,17 +290,6 @@ data class WalletState( } } -sealed interface WalletDialog : StateDialog { - data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog - object SignedHashesMultiWalletDialog : WalletDialog - object ChooseTradeActionDialog : WalletDialog - data class CurrencySelectionDialog( - val currenciesList: List, - val currentAppCurrency: FiatCurrency, - ) : WalletDialog - object RussianCardholdersWarningDialog : WalletDialog -} - enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error } enum class ErrorType { NoInternetConnection } @@ -371,39 +331,24 @@ data class Artwork( } } -data class TradeCryptoState( - val isAvailableToSell: () -> Boolean = { false }, - val isAvailableToBuy: () -> Boolean = { false }, -) { - companion object { - fun from( - exchangeManager: CurrencyExchangeManager?, - walletData: WalletData - ): TradeCryptoState { - val exchanger = exchangeManager ?: return walletData.tradeCryptoState - val currency = walletData.currency - - return TradeCryptoState( - isAvailableToSell = { exchanger.availableForSell(currency) }, - isAvailableToBuy = { exchanger.availableForBuy(currency) }, - ) - } - } -} - data class WalletData( val pendingTransactions: List = emptyList(), val hashesCountVerified: Boolean? = null, val walletAddresses: WalletAddresses? = null, val currencyData: BalanceWidgetData = BalanceWidgetData(), val updatingWallet: Boolean = false, - val tradeCryptoState: TradeCryptoState = TradeCryptoState(), val fiatRateString: String? = null, val fiatRate: BigDecimal? = null, val mainButton: WalletMainButton = WalletMainButton.SendButton(false), val currency: Currency, val walletRent: WalletRent? = null, ) { + val isAvailableToBuy: Boolean + get() = store.state.globalState.exchangeManager.availableForBuy(currency) + + val isAvailableToSell: Boolean + get() = store.state.globalState.exchangeManager.availableForSell(currency) + fun shouldShowMultipleAddress(): Boolean { val listOfAddresses = walletAddresses?.list ?: return false return listOfAddresses.size > 1 @@ -436,87 +381,6 @@ data class WalletData( private fun tokenAmountIsEmpty(): Boolean = currencyData.amount?.isZero() == true } -sealed interface Currency { - - val coinId: String? - get() = when (this) { - is Blockchain -> blockchain.toCoinId() - is Token -> token.id - } - val blockchain: com.tangem.blockchain.common.Blockchain - val currencySymbol: CryptoCurrencyName - val derivationPath: String? - - data class Token( - val token: com.tangem.blockchain.common.Token, - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String? - ) : Currency { - override val currencySymbol = token.symbol - } - - data class Blockchain( - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String? - ) : Currency { - override val currencySymbol: CryptoCurrencyName = blockchain.currency - } - - fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { - if (this is Token && this.token.id == null) return true - - if (derivationPath == null || derivationStyle == null) return false - - return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath - } - - fun isBlockchain(): Boolean = this is Blockchain - - fun isToken(): Boolean = this is Token - - companion object { - fun fromBlockchainNetwork( - blockchainNetwork: BlockchainNetwork, - token: com.tangem.blockchain.common.Token? = null - ): Currency { - return if (token != null) { - Token( - token = token, - blockchain = blockchainNetwork.blockchain, - derivationPath = blockchainNetwork.derivationPath - ) - } else { - Blockchain( - blockchain = blockchainNetwork.blockchain, - derivationPath = blockchainNetwork.derivationPath - ) - } - } - - fun fromCustomCurrency(customCurrency: CustomCurrency): Currency { - return when (customCurrency) { - is CustomCurrency.CustomBlockchain -> Blockchain( - blockchain = customCurrency.network, - derivationPath = customCurrency.derivationPath?.rawPath - ) - is CustomCurrency.CustomToken -> Token( - token = customCurrency.token, - blockchain = customCurrency.network, - derivationPath = customCurrency.derivationPath?.rawPath, - ) - } - } - - fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token { - return Token( - token = tokenWithBlockchain.token, - blockchain = tokenWithBlockchain.blockchain, - derivationPath = null - ) - } - } -} - data class WalletStore( val walletManager: WalletManager?, val blockchainNetwork: BlockchainNetwork, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt index d339e49202..f1fb6371cc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt @@ -9,7 +9,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.persistence.FiatCurrenciesPrefStorage import com.tangem.tap.scope import com.tangem.tap.store 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 ace70c1988..4030bae09b 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 @@ -3,18 +3,24 @@ package com.tangem.tap.features.wallet.redux.middlewares 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.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository +import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.demo.isDemoCard -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -27,7 +33,6 @@ class MultiWalletMiddleware { action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?, ) { val globalState = globalState ?: return -// val tapWalletManager = globalState.tapWalletManager when (action) { is WalletAction.MultiWallet.AddBlockchains -> { @@ -55,51 +60,93 @@ class MultiWalletMiddleware { blockchainNetwork = action.blockchain ) } - store.dispatch(WalletAction.LoadFiatRate( - coinsList = listOf( - Currency.Blockchain( - action.blockchain.blockchain, - action.blockchain.derivationPath + store.dispatch( + WalletAction.LoadFiatRate( + coinsList = listOf( + Currency.Blockchain( + action.blockchain.blockchain, + action.blockchain.derivationPath + ) ) ) - )) - store.dispatch(WalletAction.LoadWallet( - action.blockchain, action.walletManager - )) + ) + store.dispatch( + WalletAction.LoadWallet( + action.blockchain, action.walletManager + ) + ) } is WalletAction.MultiWallet.SaveCurrencies -> { - globalState.scanResponse?.card?.cardId?.let { - currenciesRepository.saveCurrencies(it, action.blockchainNetworks) + val cardId = action.cardId ?: globalState.scanResponse?.card?.cardId ?: return + currenciesRepository.saveCurrencies(cardId, action.blockchainNetworks) + } + is WalletAction.MultiWallet.TryToRemoveWallet -> { + val currency = action.currency + val walletManager = walletState?.getWalletManager(currency).guard { + store.dispatchErrorNotification(TapError.UnsupportedState("walletManager is NULL")) + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + return + } + + if (currency.isBlockchain() && walletManager.cardTokens.isNotEmpty()) { + store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog( + currencyTitle = currency.currencyName, + currencySymbol = currency.currencySymbol + )) + } else { + store.dispatchDialogShow(WalletDialog.RemoveWalletDialog( + currencyTitle = currency.currencyName, + onOk = { + store.dispatch(WalletAction.MultiWallet.RemoveWallet( + currency = currency, + fromScreen = AppScreen.WalletDetails + )) + store.dispatch(NavigationAction.PopBackTo()) + } + )) } } is WalletAction.MultiWallet.RemoveWallet -> { - val cardId = globalState.scanResponse?.card?.cardId - when (val currency = action.walletData.currency) { + val currency = action.currency + val cardId = globalState.scanResponse?.card?.cardId.guard { + store.dispatchErrorNotification(TapError.UnsupportedState("cardId is NULL")) + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + return + } + + when (currency) { is Currency.Blockchain -> { - cardId?.let { - currenciesRepository.removeBlockchain( - cardId = it, - blockchainNetwork = BlockchainNetwork( - currency.blockchain, currency.derivationPath, - emptyList() - ) + currenciesRepository.removeBlockchain( + cardId = cardId, + blockchainNetwork = BlockchainNetwork( + blockchain = currency.blockchain, + derivationPath = currency.derivationPath, + tokens = emptyList() ) - } + ) } is Currency.Token -> { val walletManager = walletState?.getWalletManager(currency) if (walletManager != null) { walletManager.removeToken(currency.token) - cardId?.let { - currenciesRepository.removeToken( - cardId = it, - token = currency.token, - blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) - ) - } + currenciesRepository.removeToken( + cardId = cardId, + token = currency.token, + blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) + ) } } } + if (action.fromScreen == AppScreen.AddTokens) { + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + } + } + is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit + is WalletAction.MultiWallet.BackupWallet -> { + store.state.globalState.scanResponse?.let { + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false)) + } } // is WalletAction.MultiWallet.FindBlockchainsInUse -> { // val scanResponse = globalState.scanResponse ?: return diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index ebbaae8538..319c940a52 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -11,7 +11,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens @@ -37,19 +37,18 @@ class TradeCryptoMiddleware { action: WalletAction.TradeCryptoAction.Buy, ) { if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { - store.dispatchOnMain( - WalletAction.DialogAction.RussianCardholdersWarningDialog - ) + store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog) return } val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return - val exchangeManager = store.state.globalState.exchangeManager ?: return - val appCurrency = store.state.globalState.appCurrency + val card = store.state.globalState.scanResponse?.card ?: return val addresses = selectedWalletData.walletAddresses?.list.orEmpty() if (addresses.isEmpty()) return + val exchangeManager = store.state.globalState.exchangeManager + val appCurrency = store.state.globalState.appCurrency val currency = selectedWalletData.currency if (currency is Currency.Token && currency.blockchain.isTestnet()) { @@ -59,7 +58,13 @@ class TradeCryptoMiddleware { return } - scope.launch { exchangeManager.buyErc20TestnetTokens(walletManager, currency.token) } + scope.launch { + exchangeManager.buyErc20TestnetTokens( + card = card, + walletManager = walletManager, + token = currency.token + ) + } return } @@ -74,15 +79,13 @@ class TradeCryptoMiddleware { private fun proceedSellAction() { val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return - val exchangeManager = store.state.globalState.exchangeManager ?: return - val appCurrency = store.state.globalState.appCurrency + val appCurrency = store.state.globalState.appCurrency val addresses = selectedWalletData.walletAddresses?.list.orEmpty() if (addresses.isEmpty()) return val currency = selectedWalletData.currency - - exchangeManager.getUrl( + store.state.globalState.exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Sell, blockchain = currency.blockchain, cryptoCurrencyName = currency.currencySymbol, @@ -93,8 +96,8 @@ class TradeCryptoMiddleware { private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return - val walletManager = - store.state.walletState.getWalletManager(selectedWalletData.currency) + + val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency) store.dispatchOnMain(PrepareSendScreen( coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin), coinRate = selectedWalletData.fiatRate, @@ -109,11 +112,10 @@ class TradeCryptoMiddleware { } private fun openReceiptUrl(transactionId: String) { - val exchangeManager = store.state.globalState.exchangeManager ?: return - store.dispatchOnMain(NavigationAction.PopBackTo()) - exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let { - store.dispatchOnMain(NavigationAction.OpenUrl(it)) - } + store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( + action = CurrencyExchangeManager.Action.Sell, + transactionId = transactionId, + )?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index e760c446f2..d53809be9c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt @@ -4,7 +4,7 @@ import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store class WalletDialogsMiddleware { 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 e4f7b6f659..7c63b551f9 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 @@ -1,8 +1,5 @@ package com.tangem.tap.features.wallet.redux.middlewares -import android.content.Intent -import android.net.Uri -import androidx.core.content.ContextCompat import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -15,12 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.extensions.copyToClipboard -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.onCardScanned -import com.tangem.tap.common.extensions.shareText -import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen @@ -31,9 +23,10 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.wallet.models.PendingTransactionType +import com.tangem.tap.features.wallet.models.filterByCoin import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.features.wallet.models.getSendableAmounts -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.redux.WalletState @@ -232,9 +225,7 @@ class WalletMiddleware { action.context.shareText(action.address) } is WalletAction.ExploreAddress -> { - val uri = Uri.parse(action.exploreUrl) - val intent = Intent(Intent.ACTION_VIEW, uri) - ContextCompat.startActivity(action.context, intent, null) + store.dispatchOpenUrl(action.exploreUrl) } is WalletAction.Send -> { val newAction = prepareSendAction(action.amount, store.state.walletState) @@ -332,7 +323,10 @@ class WalletMiddleware { } val balance = walletManager.wallet.fundsAvailable(AmountType.Coin) - val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing) + val outgoingTxs = walletManager.wallet.getPendingTransactions( + PendingTransactionType.Outgoing + ).filterByCoin() + val rentExempt = result.data val show = if (outgoingTxs.isEmpty()) { isNeedToShowWarning(balance, rentExempt) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 5197928d6e..7aa45f997d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -8,7 +8,6 @@ import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.isGreaterThan @@ -26,15 +25,15 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.wallet.R +import java.math.BigDecimal import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal class WarningsMiddleware { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { when (action) { - WalletAction.Warnings.Update -> setWarningMessages() + is WalletAction.Warnings.Update -> setWarningMessages() is WalletAction.Warnings.CheckIfNeeded -> { showCardWarningsIfNeeded(globalState) val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow() @@ -66,9 +65,11 @@ class WarningsMiddleware { ) } } - is WalletAction.Warnings.RestoreFundsWarningClosed -> { - preferencesStorage.saveRestoreFundsWarningClosed() - } + is WalletAction.Warnings.AppRating, + is WalletAction.Warnings.CheckHashesCount, + is WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount, + is WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline, + is WalletAction.Warnings.Set -> Unit } } @@ -93,9 +94,6 @@ class WarningsMiddleware { addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true) return@let } - if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) { - addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning()) - } showWarningLowRemainingSignaturesIfNeeded(card) if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) { @@ -190,6 +188,7 @@ class WarningsMiddleware { true ) } + null -> Unit } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt new file mode 100644 index 0000000000..68718acd45 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.features.wallet.redux.models + +import com.tangem.blockchain.common.Amount +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.common.redux.StateDialog +import com.tangem.wallet.R + +sealed interface WalletDialog : StateDialog { + data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog + object SignedHashesMultiWalletDialog : WalletDialog + object ChooseTradeActionDialog : WalletDialog + data class CurrencySelectionDialog( + val currenciesList: List, + val currentAppCurrency: FiatCurrency, + ) : WalletDialog + + data class RemoveWalletDialog( + val currencyTitle: String, + val onOk: () -> Unit + ) : WalletDialog { + val messageRes: Int = R.string.token_details_hide_alert_message + val titleRes: Int = R.string.token_details_hide_alert_title + val primaryButtonRes: Int = R.string.token_details_hide_alert_hide + } + + data class TokensAreLinkedDialog( + val currencyTitle: String, + val currencySymbol: String + ) : WalletDialog { + val messageRes: Int = R.string.token_details_unable_hide_alert_message + val titleRes: Int = R.string.token_details_unable_hide_alert_title + } + + object RussianCardholdersWarningDialog : WalletDialog +} \ No newline at end of file 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 80ccfca757..e8f9551910 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 @@ -7,9 +7,7 @@ import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFormattedCurrencyString import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.wallet.models.filterByToken -import com.tangem.tap.features.wallet.models.getPendingTransactions -import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.* import com.tangem.tap.features.wallet.redux.* import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN import com.tangem.tap.features.wallet.ui.BalanceStatus @@ -103,10 +101,10 @@ class MultiWalletReducer { } is WalletAction.MultiWallet.TokenLoaded -> { val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token) - val wallet = state.getWalletManager(currency)?.wallet.guard { - throw NullPointerException("MultiWallet.TokenLoaded: WalletManager must be no NULL") + val walletManager = state.getWalletManager(currency).guard { + throw NullPointerException("MultiWallet.TokenLoaded: WalletManager must be not NULL") } - + val wallet = walletManager.wallet val pendingTransactions = wallet.getPendingTransactions() val tokenPendingTransactions = pendingTransactions.filterByToken(action.token) val tokenBalanceStatus = when { @@ -136,7 +134,8 @@ class MultiWalletReducer { token = action.token, blockchain = action.blockchain.blockchain, derivationPath = action.blockchain.derivationPath - ) + ), + walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)) ) state.updateWalletData(newTokenWalletData) } @@ -145,20 +144,28 @@ class MultiWalletReducer { is WalletAction.MultiWallet.SelectWallet -> state.copy(selectedCurrency = action.walletData?.currency) - + is WalletAction.MultiWallet.TryToRemoveWallet -> state is WalletAction.MultiWallet.RemoveWallet -> { - state.removeWallet(action.walletData) + state.removeWallet(state.getWalletData(action.currency)) } is WalletAction.MultiWallet.SetPrimaryBlockchain -> state.copy(primaryBlockchain = action.blockchain) is WalletAction.MultiWallet.SetPrimaryToken -> state.copy(primaryToken = action.token) -// is WalletAction.MultiWallet.FindTokensInUse -> state -// is WalletAction.MultiWallet.FindBlockchainsInUse -> state is WalletAction.MultiWallet.SaveCurrencies -> state + is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy( + showBackupWarning = action.show + ) + is WalletAction.MultiWallet.BackupWallet -> state } } + + private fun findWalletRent(walletStore: WalletStore?): WalletRent? { + return walletStore?.walletsData?.firstOrNull { + it.walletRent != null + }?.walletRent + } } private fun addTokens( 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 50f4a1cec0..63d378718f 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 @@ -9,10 +9,13 @@ import com.tangem.tap.common.extensions.toFormattedCurrencyString import com.tangem.tap.common.extensions.toFormattedFiatValue import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.filterByToken import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.features.wallet.models.removeUnknownTransactions -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData @@ -37,8 +40,6 @@ class OnWalletLoadedReducer { val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState val fiatCurrency = store.state.globalState.appCurrency - val exchangeManager = store.state.globalState.exchangeManager - val coinAmountValue = wallet.amounts[AmountType.Coin]?.value val formattedAmount = coinAmountValue?.toFormattedCurrencyString( wallet.blockchain.decimals(), @@ -70,7 +71,6 @@ class OnWalletLoadedReducer { pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled), currency = Currency.fromBlockchainNetwork(blockchainNetwork), - tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData), ) val tokens = wallet.getTokens().mapNotNull { token -> @@ -103,7 +103,6 @@ class OnWalletLoadedReducer { ), pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled), - tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData), ) } val newWallets = tokens + newWalletData @@ -117,8 +116,6 @@ class OnWalletLoadedReducer { if (wallet.blockchain != walletState.primaryBlockchain) return walletState val fiatCurrencyName = store.state.globalState.appCurrency.code - val exchangeManager = store.state.globalState.exchangeManager - val token = wallet.getFirstToken() val tokenData = if (token != null) { val tokenAmount = wallet.getTokenAmount(token) @@ -166,7 +163,6 @@ class OnWalletLoadedReducer { ), pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(sendButtonEnabled), - tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet), ) val wallets = listOfNotNull(walletData) val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets) 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 3e39e7ed42..a4b679512e 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 @@ -6,17 +6,30 @@ import com.tangem.blockchain.common.Wallet import com.tangem.common.extensions.mapNotNullValues import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.toFiatRateString +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFiatValue +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedFiatValue import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.WalletRent -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.AddressData +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 +import com.tangem.tap.features.wallet.redux.WalletAddresses +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState +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 com.tangem.tap.store import org.rekotlin.Action import java.math.BigDecimal @@ -34,14 +47,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState { if (action !is WalletAction) return state.walletState - val exchangeManager = store.state.globalState.exchangeManager var newState = state.walletState when (action) { is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState) is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) - is WalletAction.ResetState -> newState = WalletState() + is WalletAction.ResetState -> newState = WalletState(cardId = action.newCardId) is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet) is WalletAction.EmptyWallet -> { newState = newState.copy( @@ -143,10 +155,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState { currencySymbol = walletData.currencyData.currencySymbol, ), mainButton = WalletMainButton.SendButton(false), - tradeCryptoState = TradeCryptoState.from( - exchangeManager, - walletData - ) ) } ) @@ -170,13 +178,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState { currencySymbol = wallet.currencyData.currencySymbol, ), mainButton = WalletMainButton.SendButton(false), - tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet) ) } - val wallets = newState.updateTradeCryptoState( - exchangeManager, - newState.replaceSomeWallets(newWallets) - ) + val wallets = newState.replaceSomeWallets(newWallets) val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets) newState = newState.updateWalletStore(walletStore) } @@ -209,14 +213,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) ) } - var updatedWalletStore = newState.getWalletStore(action.blockchain) + val updatedWalletStore = newState.getWalletStore(action.blockchain) ?.updateWallets(listOfNotNull(walletData)) - updatedWalletStore = - updatedWalletStore?.updateWallets( - newState.updateTradeCryptoState(exchangeManager, updatedWalletStore.walletsData) - ) - newState = newState.updateWalletStore(updatedWalletStore) } @@ -247,11 +246,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) ) } - val updatedWallets = - newState.updateTradeCryptoState( - exchangeManager, - walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData - ) + val updatedWallets = walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData newState = newState.updateWalletsData(updatedWallets) 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 1cf3a038b2..ab07dd33d5 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 @@ -1,7 +1,11 @@ package com.tangem.tap.features.wallet.ui import android.os.Bundle -import android.view.* +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.annotation.ColorRes @@ -11,20 +15,32 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.TestActions -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.appendIfNotNull +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.fitChipsByGroupWidth +import com.tangem.tap.common.extensions.getColor +import com.tangem.tap.common.extensions.getString +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.recyclerView.SpaceItemDecoration import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.getQRReceiveMessage +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.ErrorType +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN 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.loadCurrencyIcon import com.tangem.tap.features.wallet.ui.test.TestWalletDetails import com.tangem.tap.store import com.tangem.wallet.R @@ -124,7 +140,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), setupAddressCard(selectedWallet) setupNoInternetHandling(state) setupBalanceData(selectedWallet.currencyData) - setupButtons(selectedWallet) + setupButtons(selectedWallet, state.isExchangeServiceFeatureOn) handleCurrencyIcon(selectedWallet) handleWarnings(selectedWallet) @@ -138,7 +154,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), selectedWallet.currency.derivationPath, emptyList() ) - )) + ) + ) } } @@ -163,15 +180,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), private fun setupCurrency(currencyData: BalanceWidgetData, currency: Currency) = with(binding) { tvCurrencyTitle.text = currencyData.currency - if (currency is Currency.Token) { - tvCurrencySubtitle.text = currency.blockchain.tokenDisplayName() - tvCurrencySubtitle.show() - } else { - tvCurrencySubtitle.hide() - } + tvCurrencySubtitle.text = tvCurrencySubtitle.getString( + R.string.wallet_currency_subtitle, + currency.blockchain.fullName + ) } - private fun setupButtons(selectedWallet: WalletData) = with(binding) { + private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) { lWalletDetails.btnCopy.setOnClickListener { selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) @@ -184,8 +199,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } rowButtons.updateButtonsVisibility( - buyAllowed = selectedWallet.tradeCryptoState.isAvailableToBuy(), - sellAllowed = selectedWallet.tradeCryptoState.isAvailableToSell(), + exchangeServiceFeatureOn = isExchangeServiceFeatureOn, + buyAllowed = selectedWallet.isAvailableToBuy, + sellAllowed = selectedWallet.isAvailableToSell, sendAllowed = selectedWallet.mainButton.enabled, ) } @@ -199,11 +215,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) { - loadCurrencyIcon( - currencyImageView = ivCurrency, - currencyTextView = tvTokenLetter, - blockchain = wallet.currency.blockchain, - token = (wallet.currency as? Currency.Token)?.token + ivCurrency.load( + currency = wallet.currency, + derivationStyle = store.state.globalState + .scanResponse + ?.card + ?.derivationStyle, ) } @@ -295,7 +312,10 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), lBalance.root.show() lBalance.groupBalance.hide() lBalance.tvError.show() - lBalance.tvError.setWarningStatus(R.string.wallet_balance_blockchain_unreachable, data.errorMessage) + lBalance.tvError.setWarningStatus( + R.string.wallet_balance_blockchain_unreachable, + data.errorMessage + ) } BalanceStatus.NoAccount -> { lBalance.root.hide() @@ -314,9 +334,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), return when (item.itemId) { R.id.menu_remove -> { store.state.walletState.getSelectedWalletData()?.let { walletData -> - store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData)) - store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData.currency)) true } false @@ -327,10 +345,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.wallet_details, menu) - val walletCanBeRemoved = store.state.walletState.canBeRemoved( - store.state.walletState.getSelectedWalletData() - ) - menu.getItem(0).isEnabled = walletCanBeRemoved } private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) { @@ -346,7 +360,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), setStatus(getString(mainMessage), R.color.darkGray4, null) } - private fun TextView.setStatus(text: String, @ColorRes color: Int, @DrawableRes drawable: Int?) { + private fun TextView.setStatus( + text: String, + @ColorRes color: Int, + @DrawableRes drawable: Int? + ) { this.text = text setTextColor(getColor(color)) setCompoundDrawablesWithIntrinsicBounds(drawable ?: 0, 0, 0, 0) 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 176fa48a14..1389600f94 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 @@ -13,6 +13,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding import coil.load +import coil.size.Scale +import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.tap.MainActivity import com.tangem.tap.common.extensions.show import com.tangem.tap.common.recyclerView.SpaceItemDecoration @@ -31,6 +33,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView +import com.tangem.tap.features.wallet.ui.wallet.SaltPaySingleWalletView import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.store @@ -112,16 +115,25 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + walletView = SaltPaySingleWalletView() + walletView.changeWalletView(this, binding) + } + state.isMultiwalletAllowed && state.primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && + walletView !is MultiWalletView -> { + walletView = MultiWalletView() + walletView.changeWalletView(this, binding) + } + !state.isMultiwalletAllowed && walletView !is SingleWalletView -> { + walletView = SingleWalletView() + walletView.changeWalletView(this, binding) + } + else -> {} // we keep the same view unless we scan a card that requires a different view } + walletView.onNewState(state) if (!state.shouldShowDetails) { @@ -133,7 +145,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.drawable.ic_arrow_right_20 PendingTransactionType.Unknown -> return } - binding.tvPendingTransaction.text = binding.root.getString(transactionDescriptionRes) + binding.tvPendingTransaction.text = + binding.root.getString(transactionDescriptionRes).let { "$it " } transaction.amountValueUi?.let { binding.tvPendingTransactionAmount.text = "$it " } - binding.tvPendingTransactionCurrency.text = "${transaction.currency}" + binding.tvPendingTransactionCurrency.text = transaction.currency if (transaction.address != null) { binding.tvPendingTransactionAddress.text = 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 26f5b8da86..4be9104bb1 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 @@ -12,11 +12,9 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.ui.BalanceStatus -import com.tangem.tap.features.wallet.ui.images.loadCurrencyIcon import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.ItemCurrencyWalletBinding @@ -66,12 +64,6 @@ class WalletAdapter // Skip changes when on refreshing status if (status == BalanceStatus.Refreshing) return@with - val isCustomCurrency = wallet.currency.isCustomCurrency( - derivationStyle = store.state.globalState - .scanResponse - ?.card - ?.derivationStyle - ) val statusMessage = when (status) { BalanceStatus.TransactionInProgress -> { root.getString(R.string.wallet_balance_tx_in_progress) @@ -86,7 +78,8 @@ class WalletAdapter BalanceStatus.UnknownBlockchain, BalanceStatus.Loading, BalanceStatus.Refreshing, - null -> null + null, + -> null } if (status == null || status == BalanceStatus.Loading) { @@ -97,11 +90,12 @@ class WalletAdapter lContent.root.show() } - loadCurrencyIcon( - currencyImageView = ivCurrency, - currencyTextView = tvTokenLetter, - token = (wallet.currency as? Currency.Token)?.token, - blockchain = wallet.currency.blockchain, + ivCurrency.load( + currency = wallet.currency, + derivationStyle = store.state.globalState + .scanResponse + ?.card + ?.derivationStyle, ) lContent.tvCurrency.text = wallet.currencyData.currency @@ -115,8 +109,6 @@ class WalletAdapter lContent.tvExchangeRate.text = wallet.fiatRateString ?: root.getString(id = R.string.token_item_no_rate) - badgeCustomBalance.isVisible = isCustomCurrency - cardWallet.setOnClickListener { store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 9014d5d4ff..dee8fef67c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -1,25 +1,21 @@ package com.tangem.tap.features.wallet.ui.adapters -import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.os.ConfigurationCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.play.core.review.ReviewManagerFactory import com.tangem.tap.common.analytics.AnalyticsEvent -import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.getActivity import com.tangem.tap.common.extensions.getColor import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.features.feedback.RateCanBeBetterEmail import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R @@ -89,26 +85,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi binding.btnClose.hide() val buttonAction = - when { - warning.titleResId == R.string.warning_important_security_info -> { + when (warning.titleResId) { + R.string.warning_important_security_info -> { View.OnClickListener { store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog) } } - warning.messageResId == R.string.alert_funds_restoration_message -> { - binding.btnClose.show() - binding.btnClose.setOnClickListener { - store.dispatch(GlobalAction.HideWarningMessage(warning)) - store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed) - } - val locale = ConfigurationCompat - .getLocales(Resources.getSystem().configuration) - .get(0) - val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language) - View.OnClickListener { - store.dispatchOpenUrl(url) - } - } else -> { View.OnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) @@ -135,7 +117,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) store.dispatch(GlobalAction.HideWarningMessage(warning)) - store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail())) + store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) } binding.btnReallyCool.setOnClickListener { val activity = binding.root.context.getActivity() ?: return@setOnClickListener diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendBottomSheetDialog.kt index 2f9e597096..ff82942f2b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendBottomSheetDialog.kt @@ -5,16 +5,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.view.ContextThemeWrapper -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.* import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.blockchain.common.Amount import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.DialogWalletSendBinding diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index 496a8ff731..91afcc3819 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.feedback.ScanFailsEmail import com.tangem.tap.store import com.tangem.wallet.R @@ -19,7 +19,7 @@ class ScanFailsDialog { setTitle(context.getString(R.string.common_warning)) setMessage(R.string.alert_troubleshooting_scan_card_title) setPositiveButton(R.string.alert_button_request_support) { _, _ -> - store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail())) + store.dispatch(GlobalAction.SendEmail(ScanFailsEmail())) } setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> } setOnDismissListener { store.dispatchDialogHide() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt index 0561426bad..cd7959319e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt @@ -5,7 +5,6 @@ import android.widget.TextView import androidx.constraintlayout.utils.widget.ImageFilterView import coil.imageLoader import coil.request.ImageRequest -import coil.transform.RoundedCornersTransformation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchain.common.Token @@ -20,22 +19,7 @@ import com.tangem.wallet.R private const val QCX = "QCX" private const val VOYR = "VOYRME" -fun loadCurrencyIcon( - currencyImageView: ImageFilterView, - currencyTextView: TextView, - token: Token?, - blockchain: Blockchain, -) { - CurrencyIconLoader( - currencyImageView = currencyImageView, - currencyTextView = currencyTextView, - token = token, - blockchain = blockchain - ) - .load() -} - -private class CurrencyIconLoader( +class CurrencyIconRequest( private val currencyImageView: ImageFilterView, private val currencyTextView: TextView, private val token: Token?, @@ -135,9 +119,6 @@ private inline fun ImageView.loadIcon( .placeholder(placeholderRes) .error(placeholderRes) .fallback(placeholderRes) - .transformations( - RoundedCornersTransformation(radius = 8f) - ) .listener( onStart = { onStart() }, onSuccess = { _, _ -> onSuccess() }, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt new file mode 100644 index 0000000000..ec9ff38519 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.features.wallet.ui.images + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.constraintlayout.utils.widget.ImageFilterView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.tap.common.extensions.getRoundIconRes +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.wallet.databinding.ViewCurrencyIconBinding +import kotlin.math.roundToInt + +class CurrencyIconView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : ConstraintLayout(context, attrs, defStyleAttr) { + private val binding = ViewCurrencyIconBinding.inflate( + LayoutInflater.from(context), + this, + ) + + private val currencyImageView: ImageFilterView + get() = binding.ivCurrency + + private val currencyTextView: TextView + get() = binding.tvTokenLetter + + private var isBlockchainIconVisible: Boolean + get() = binding.ivBlockchain.isVisible + set(value) = binding.ivBlockchain::isVisible.set(value) + + private var isBadgeVisible: Boolean + get() = binding.badge.isVisible + set(value) = binding.badge::isVisible.set(value) + + @DrawableRes + private var blockchainIconRes: Int? = null + set(value) { + if (value != null && value != field && isBlockchainIconVisible) { + binding.ivBlockchain.setImageResource(value) + field = value + } + } + + init { + minWidth = dpToPx(48f).roundToInt() + minHeight = dpToPx(48f).roundToInt() + } + + fun load( + currency: Currency, + derivationStyle: DerivationStyle?, + ) { + isBlockchainIconVisible = currency.isToken() + isBadgeVisible = currency.isCustomCurrency(derivationStyle) + blockchainIconRes = currency.blockchain.getRoundIconRes() + + CurrencyIconRequest( + currencyImageView = currencyImageView, + currencyTextView = currencyTextView, + token = (currency as? Currency.Token)?.token, + blockchain = currency.blockchain, + ).load() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/WalletDetailsButtonsRow.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/WalletDetailsButtonsRow.kt index 12efe010c8..582b5f178c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/WalletDetailsButtonsRow.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/WalletDetailsButtonsRow.kt @@ -14,9 +14,8 @@ internal class WalletDetailsButtonsRow @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr) { private val binding = ViewWalletDetailsButtonsRowBinding.inflate( LayoutInflater.from(context), - this + this, ) - var onBuyClick: (() -> Unit)? = null var onSellClick: (() -> Unit)? = null var onTradeClick: (() -> Unit)? = null @@ -34,10 +33,13 @@ internal class WalletDetailsButtonsRow @JvmOverloads constructor( } fun updateButtonsVisibility( + exchangeServiceFeatureOn: Boolean, buyAllowed: Boolean, sellAllowed: Boolean, - sendAllowed: Boolean + sendAllowed: Boolean, ) = with(binding) { + containerExchangeButtons.isVisible = exchangeServiceFeatureOn + btnBuy.isVisible = (buyAllowed && !sellAllowed) || (!buyAllowed && !sellAllowed) btnBuy.isEnabled = buyAllowed btnSell.isVisible = !buyAllowed && sellAllowed diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt index f47a0b3ea1..6cce3b391b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.wallet.ui.wallet import android.content.Context import androidx.appcompat.app.AlertDialog import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R 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 5646b437f0..73504ddbfc 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.ui.wallet +import android.widget.Button import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.common.card.Card @@ -20,33 +21,26 @@ import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter +import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletBinding - -class MultiWalletView : WalletView { - - private var fragment: WalletFragment? = null - private var binding: FragmentWalletBinding? = null - +class MultiWalletView : WalletView() { private lateinit var walletsAdapter: WalletAdapter - - override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { setFragment(fragment, binding) onViewCreated() showMultiWalletView(binding) } - private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) { tvTwinCardNumber.hide() rvPendingTransaction.hide() lCardBalance.root.hide() lAddress.root.hide() - lButtonsShort.root.hide() - lButtonsLong.root.hide() + rowButtons.hide() + lSingleWalletBalance.root.hide() rvMultiwallet.show() btnAddToken.show() setupWalletCardNumber(binding) @@ -64,16 +58,6 @@ class MultiWalletView : WalletView { } } - override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { - this.fragment = fragment - this.binding = binding - } - - override fun removeFragment() { - this.fragment = null - this.binding = null - } - override fun onViewCreated() { setupWalletsRecyclerView() } @@ -91,6 +75,7 @@ class MultiWalletView : WalletView { val binding = binding ?: return handleTotalBalance(binding, state.totalBalance) + handleBackupWarning(binding, state.showBackupWarning) walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken) binding.btnAddToken.setOnClickListener { @@ -99,27 +84,33 @@ class MultiWalletView : WalletView { TokensAction.LoadCurrencies( supportedBlockchains = currenciesRepository.getBlockchains( card.firmwareVersion, - card.isTestCard + card.isTestCard, ), - scanResponse = store.state.globalState.scanResponse - ) + scanResponse = store.state.globalState.scanResponse, + ), ) store.dispatch(TokensAction.AllowToAddTokens(true)) store.dispatch( TokensAction.SetAddedCurrencies( wallets = state.walletsData, - derivationStyle = card.derivationStyle - ) - ) - store.dispatch( - TokensAction.SetNonRemovableCurrencies( - state.walletsData.filterNot { state.canBeRemoved(it) }) + derivationStyle = card.derivationStyle, + ), ) store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } + private fun handleBackupWarning( + binding: FragmentWalletBinding, + showBackupWarning: Boolean, + ) = with(binding.lWalletBackupWarning) { + root.isVisible = showBackupWarning + root.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.BackupWallet) + } + } + private fun handleTotalBalance( binding: FragmentWalletBinding, totalBalance: TotalBalance?, @@ -135,11 +126,11 @@ class MultiWalletView : WalletView { veilBalance.unVeil() } tvProcessing.animateVisibility( - show = totalBalance.state == ProgressState.Error + show = totalBalance.state == ProgressState.Error, ) tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString( - currencySymbol = totalBalance.fiatCurrency.symbol + currencySymbol = totalBalance.fiatCurrency.symbol, ) tvCurrencyName.text = totalBalance.fiatCurrency.code @@ -152,14 +143,14 @@ class MultiWalletView : WalletView { private fun handleErrorStates( state: WalletState, binding: FragmentWalletBinding, - fragment: WalletFragment + fragment: WalletFragment, ) { when (state.primaryWallet?.currencyData?.status) { BalanceStatus.EmptyCard -> { showErrorState( binding, fragment.getText(R.string.wallet_error_empty_card), - fragment.getString(R.string.wallet_error_empty_card_subtitle) + fragment.getString(R.string.wallet_error_empty_card_subtitle), ) configureButtonsForEmptyWalletState(binding) } @@ -167,7 +158,7 @@ class MultiWalletView : WalletView { showErrorState( binding, fragment.getText(R.string.wallet_error_unsupported_blockchain), - fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) + fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle), ) } else -> { /* no-op */ @@ -191,9 +182,21 @@ class MultiWalletView : WalletView { private fun configureButtonsForEmptyWalletState(binding: FragmentWalletBinding) = with(binding) { - lButtonsLong.root.show() - lButtonsLong.btnConfirmLong.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } - lButtonsLong.btnConfirmLong.text = - fragment?.getText(R.string.wallet_button_create_wallet) + rowButtons.btnBuy.hide() + rowButtons.btnSell.hide() + rowButtons.btnTrade.hide() + rowButtons.show() + + rowButtons.btnSend.text = fragment?.getText(R.string.wallet_button_create_wallet) + rowButtons.onSendClick = { store.dispatch(WalletAction.CreateWallet) } } -} \ No newline at end of file +} + +private val WalletDetailsButtonsRow.btnBuy: Button + get() = this.findViewById(R.id.btn_buy) +private val WalletDetailsButtonsRow.btnSell: Button + get() = this.findViewById(R.id.btn_sell) +private val WalletDetailsButtonsRow.btnTrade: Button + get() = this.findViewById(R.id.btn_trade) +private val WalletDetailsButtonsRow.btnSend: Button + get() = this.findViewById(R.id.btn_send) \ No newline at end of file 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 new file mode 100644 index 0000000000..95509f5b37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.features.wallet.ui.wallet + +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.store +import com.tangem.wallet.databinding.LayoutSingleWalletBalanceBinding +import java.math.BigDecimal + +data class SaltPayBalanceWidgetData( + val state: ProgressState? = null, + val currencySymbol: String? = null, + val currency: String? = null, + val fiatAmount: BigDecimal? = null, + val fiatCurrency: FiatCurrency? = null, +) + +class SaltPayBalanceWidget( + private val binding: LayoutSingleWalletBalanceBinding, + private val data: SaltPayBalanceWidgetData, +) { + fun setup(): Unit = with(binding) { + if (data.state == ProgressState.Loading) { + veilBalance.veil() + veilBalanceCrypto.veil() + } else { + veilBalance.unVeil() + veilBalanceCrypto.unVeil() + } + tvProcessing.animateVisibility( + show = data.state == ProgressState.Error, + ) + veilBalanceCrypto.animateVisibility( + show = data.state != ProgressState.Error, + ) + tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString( + currencySymbol = data.fiatCurrency?.symbol ?: "", + ) + tvBalanceCrypto.text = data.currency + + tvCurrencyName.text = data.fiatCurrency?.code + + tvCurrencyName.setOnClickListener { + store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) + } + } +} + + 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 new file mode 100644 index 0000000000..09d00b294e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt @@ -0,0 +1,51 @@ +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.WalletFragment +import com.tangem.tap.store +import com.tangem.wallet.databinding.FragmentWalletBinding + +class SaltPaySingleWalletView : WalletView() { + override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { + setFragment(fragment, binding) + onViewCreated() + showSingleWalletView(binding) + } + + private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { + rvMultiwallet.hide() + btnAddToken.hide() + rvPendingTransaction.hide() + tvTwinCardNumber.hide() + lCardBalance.root.hide() + lAddress.root.hide() + lSingleWalletBalance.root.show() + } + + override fun onViewCreated() { + } + + override fun onNewState(state: WalletState) { + val binding = binding ?: return + state.primaryWallet ?: return + + setupBalance(state, state.primaryWallet, binding) + } + + private fun setupBalance(state: WalletState, primaryWallet: WalletData, binding: FragmentWalletBinding) { + binding.lSingleWalletBalance.root.show() + SaltPayBalanceWidget( + binding = binding.lSingleWalletBalance, + data = SaltPayBalanceWidgetData( + state = state.state, + currencySymbol = primaryWallet.currencyData.currencySymbol, + currency = primaryWallet.currencyData.amountFormatted, + fiatAmount = primaryWallet.currencyData.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 7c431c33b7..ab271db8fb 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 @@ -2,7 +2,6 @@ package com.tangem.tap.features.wallet.ui.wallet import android.view.View import android.view.ViewGroup -import android.widget.Button import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.beginDelayedTransition @@ -10,32 +9,23 @@ import com.tangem.tap.common.extensions.fitChipsByGroupWidth import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.BalanceWidget import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletBinding -class SingleWalletView : WalletView { - +class SingleWalletView : WalletView() { private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter - private var fragment: WalletFragment? = null - private var binding: FragmentWalletBinding? = null - - override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { - this.fragment = fragment - this.binding = binding - } - - override fun removeFragment() { - fragment = null - binding = null - } - override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { setFragment(fragment, binding) onViewCreated() @@ -48,13 +38,13 @@ class SingleWalletView : WalletView { rvPendingTransaction.hide() lCardBalance.root.show() lAddress.root.show() + lSingleWalletBalance.root.hide() } override fun onViewCreated() { setupTransactionsRecyclerView() } - private fun setupTransactionsRecyclerView() { val fragment = fragment ?: return pendingTransactionAdapter = PendingTransactionsAdapter() @@ -64,12 +54,11 @@ class SingleWalletView : WalletView { } override fun onNewState(state: WalletState) { - val fragment = fragment ?: return val binding = binding ?: return state.primaryWallet ?: return setupTwinCards(state.twinCardsState, binding) - setupButtons(state.primaryWallet, state.isTangemTwins, binding) + setupButtons(state.primaryWallet, binding, state.isExchangeServiceFeatureOn) setupAddressCard(state.primaryWallet, binding) showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) setupBalance(state, state.primaryWallet) @@ -80,7 +69,6 @@ class SingleWalletView : WalletView { binding?.rvPendingTransaction?.show(pendingTransactions.isNotEmpty()) } - private fun setupBalance(state: WalletState, primaryWallet: WalletData) { val fragment = fragment ?: return binding?.apply { @@ -89,14 +77,12 @@ class SingleWalletView : WalletView { binding = this.lCardBalance, fragment = fragment, data = primaryWallet.currencyData, - isTwinCard = state.isTangemTwins + isTwinCard = state.isTangemTwins, ).setup() } } - private fun setupTwinCards( - twinCardsState: TwinCardsState?, binding: FragmentWalletBinding - ) = with(binding) { + private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) { twinCardsState?.cardNumber?.let { cardNumber -> tvTwinCardNumber.show() val number = when (cardNumber) { @@ -112,101 +98,53 @@ class SingleWalletView : WalletView { } private fun setupButtons( - state: WalletData, isTwinsWallet: Boolean, binding: FragmentWalletBinding + walletData: WalletData, + binding: FragmentWalletBinding, + isExchangeServiceFeatureEnabled: Boolean, ) = with(binding) { - - setupButtonsType(state, binding) - - val tradeState = state.tradeCryptoState - val btnConfirm = if (tradeState.isAvailableToSell() || tradeState.isAvailableToBuy()) { - lButtonsShort.btnConfirm - } else { - lButtonsLong.btnConfirmLong - } - - setupConfirmButton(state, btnConfirm, isTwinsWallet) + setupRowButtons(walletData, rowButtons, isExchangeServiceFeatureEnabled) lAddress.btnCopy.setOnClickListener { - state.walletAddresses?.selectedAddress?.address?.let { addressString -> + walletData.walletAddresses?.selectedAddress?.address?.let { addressString -> store.dispatch(WalletAction.CopyAddress(addressString, fragment!!.requireContext())) } } lAddress.btnShowQr.setOnClickListener { - state.walletAddresses?.selectedAddress?.let { selectedAddress -> + walletData.walletAddresses?.selectedAddress?.let { selectedAddress -> store.dispatch( WalletAction.DialogAction.QrCode( - currency = state.currency, - selectedAddress = selectedAddress - ) + currency = walletData.currency, + selectedAddress = selectedAddress, + ), ) } } - - setupTradeButton(binding, state.tradeCryptoState) } - private fun setupTradeButton(binding: FragmentWalletBinding, tradeCryptoState: TradeCryptoState) { - val allowedToBuy = tradeCryptoState.isAvailableToBuy() - val allowedToSell = tradeCryptoState.isAvailableToSell() - val action = when { - allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy() - !allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell - allowedToBuy && allowedToSell -> WalletAction.DialogAction.ChooseTradeActionDialog - else -> null - } - val text = when { - allowedToBuy && !allowedToSell -> R.string.wallet_button_buy - !allowedToBuy && allowedToSell -> R.string.wallet_button_sell - allowedToBuy && allowedToSell -> R.string.wallet_button_trade - else -> R.string.wallet_button_trade - } - val icon = when { - allowedToBuy && !allowedToSell -> R.drawable.ic_arrow_up - !allowedToBuy && allowedToSell -> R.drawable.ic_arrow_down - allowedToBuy && allowedToSell -> R.drawable.ic_arrows_up_down - else -> null - } - with(binding) { - lButtonsShort.btnTrade.text = fragment?.getText(text) - icon?.let { lButtonsShort.btnTrade.setIconResource(it) } - lButtonsShort.btnTrade.setOnClickListener { if (action != null) store.dispatch(action) } - } - } - - private fun setupButtonsType(state: WalletData, binding: FragmentWalletBinding) = with(binding) { - if (state.tradeCryptoState.isAvailableToSell() || state.tradeCryptoState.isAvailableToBuy()) { - lButtonsLong.root.hide() - lButtonsShort.root.show() - } else { - lButtonsLong.root.show() - lButtonsShort.root.hide() - } - } - - private fun setupConfirmButton( - state: WalletData, btnConfirm: Button, isTwinsWallet: Boolean, + private fun setupRowButtons( + walletData: WalletData, + rowButtons: WalletDetailsButtonsRow, + isExchangeServiceFeatureEnabled: Boolean, ) { - val buttonTitle = when (state.mainButton) { - is WalletMainButton.SendButton -> R.string.wallet_button_send - is WalletMainButton.CreateWalletButton -> { - if (!isTwinsWallet) { - R.string.wallet_button_create_wallet - } else { - R.string.wallet_button_create_twin_wallet - } - } - } - btnConfirm.text = fragment?.getString(buttonTitle) - btnConfirm.isEnabled = state.mainButton.enabled - btnConfirm.setOnClickListener { - when (state.mainButton) { + rowButtons.updateButtonsVisibility( + exchangeServiceFeatureOn = isExchangeServiceFeatureEnabled, + buyAllowed = walletData.isAvailableToBuy, + sellAllowed = walletData.isAvailableToSell, + sendAllowed = walletData.mainButton.enabled, + ) + + rowButtons.onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) } + rowButtons.onSendClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) } + rowButtons.onTradeClick = { store.dispatch(WalletAction.DialogAction.ChooseTradeActionDialog) } + + rowButtons.onSendClick = { + when (walletData.mainButton) { is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) } } } - private fun setupAddressCard(state: WalletData, binding: FragmentWalletBinding) = with(binding.lAddress) { if (state.walletAddresses != null && state.currency is Currency.Blockchain) { binding.lAddress.root.show() @@ -214,7 +152,6 @@ class SingleWalletView : WalletView { (binding.lAddress.root as? ViewGroup)?.beginDelayedTransition() chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) @@ -233,8 +170,8 @@ class SingleWalletView : WalletView { store.dispatch( WalletAction.ExploreAddress( state.walletAddresses.selectedAddress.exploreUrl, - fragment!!.requireContext() - ) + fragment!!.requireContext(), + ), ) } } else { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt index 1d9ea0eac0..a3b09aab12 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -4,16 +4,20 @@ import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.wallet.databinding.FragmentWalletBinding -interface WalletView { +abstract class WalletView { + protected var fragment: WalletFragment? = null + protected var binding: FragmentWalletBinding? = null + fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { + this.fragment = fragment + this.binding = binding + } - fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) - - fun removeFragment() - - fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) - - fun onViewCreated() - - fun onNewState(state: WalletState) + fun removeFragment() { + fragment = null + binding = null + } + abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) + abstract fun onViewCreated() + abstract fun onNewState(state: WalletState) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt new file mode 100644 index 0000000000..a7f7b9aec2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -0,0 +1,60 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.features.wallet.models.Currency + +/** +[REDACTED_AUTHOR] + */ +class CardExchangeRules( + val cardProvider: () -> Card?, +) : ExchangeRules { + + override fun featureIsSwitchedOn(): Boolean { + val card = cardProvider() ?: return false + + return !card.isStart2Coin + } + + override fun isBuyAllowed(): Boolean { + val card = cardProvider() ?: return false + + return when { + card.isDemoCard() -> true + card.isStart2Coin -> false + else -> true + } + } + + override fun isSellAllowed(): Boolean { + val card = cardProvider() ?: return false + + return when { + card.isDemoCard() -> false + card.isStart2Coin -> false + else -> true + } + } + + override fun availableForBuy(currency: Currency): Boolean { + val card = cardProvider() ?: return false + + return when { + card.isDemoCard() -> true + card.isStart2Coin -> false + else -> true + } + } + + override fun availableForSell(currency: Currency): Boolean { + val card = cardProvider() ?: return false + + return when { + card.isDemoCard() -> false + card.isStart2Coin -> false + else -> true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 745ee01053..04c92fc542 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -7,11 +7,12 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result +import com.tangem.common.card.Card import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import com.tangem.tap.tangemSdk import java.math.BigDecimal @@ -22,17 +23,26 @@ import java.math.BigDecimal class CurrencyExchangeManager( private val buyService: ExchangeService, private val sellService: ExchangeService, + private val primaryRules: ExchangeRules, ) : ExchangeService, ExchangeUrlBuilder { + override fun featureIsSwitchedOn(): Boolean = primaryRules.featureIsSwitchedOn() + override suspend fun update() { buyService.update() sellService.update() } - override fun isBuyAllowed(): Boolean = buyService.isBuyAllowed() - override fun isSellAllowed(): Boolean = sellService.isSellAllowed() - override fun availableForBuy(currency: Currency): Boolean = buyService.availableForBuy(currency) - override fun availableForSell(currency: Currency): Boolean = sellService.availableForSell(currency) + override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed() + override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed() + + override fun availableForBuy(currency: Currency): Boolean { + return primaryRules.availableForBuy(currency) && buyService.availableForBuy(currency) + } + + override fun availableForSell(currency: Currency): Boolean { + return primaryRules.availableForSell(currency) && sellService.availableForSell(currency) + } override fun getUrl( action: Action, @@ -66,9 +76,21 @@ class CurrencyExchangeManager( } enum class Action { Buy, Sell } + + companion object { + fun dummy(): CurrencyExchangeManager = CurrencyExchangeManager( + buyService = ExchangeService.dummy(), + sellService = ExchangeService.dummy(), + primaryRules = ExchangeRules.dummy(), + ) + } } -suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) { +suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( + card: Card, + walletManager: EthereumWalletManager, + token: Token, +) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) @@ -85,7 +107,9 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: Ethereu val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) val signer = TangemSigner( - tangemSdk = tangemSdk, Message() + card = card, + tangemSdk = tangemSdk, + initialMessage = Message(), ) { signResponse -> store.dispatch( GlobalAction.UpdateWalletSignedHashes( diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index e08e007029..8d6e8e2187 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -1,16 +1,44 @@ package com.tangem.tap.network.exchangeServices import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.common.feature.Feature +import com.tangem.tap.features.wallet.models.Currency -interface ExchangeService { - suspend fun update() +interface Exchanger { fun isBuyAllowed(): Boolean fun isSellAllowed(): Boolean fun availableForBuy(currency: Currency):Boolean fun availableForSell(currency: Currency):Boolean } +interface ExchangeService: Feature, Exchanger { + suspend fun update() + + companion object { + fun dummy(): ExchangeService = object : ExchangeService { + override fun featureIsSwitchedOn(): Boolean = false + override suspend fun update() {} + override fun isBuyAllowed(): Boolean = false + override fun isSellAllowed(): Boolean = false + override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForSell(currency: Currency): Boolean = false + } + } +} + +interface ExchangeRules: Feature, Exchanger { + + companion object { + fun dummy(): ExchangeRules = object : ExchangeRules { + override fun featureIsSwitchedOn(): Boolean = false + override fun isBuyAllowed(): Boolean = false + override fun isSellAllowed(): Boolean = false + override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForSell(currency: Currency): Boolean = false + } + } +} + interface ExchangeUrlBuilder { fun getUrl( action: CurrencyExchangeManager.Action, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt index 45494e0114..87565c3640 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -4,15 +4,6 @@ import com.squareup.moshi.Json import retrofit2.http.GET import retrofit2.http.Path -/** -[REDACTED_AUTHOR] - */ - - - -private val CurrenciesUrl = "https://api.mercuryo.io/v1.6/lib/currencies" - - interface MercuryoApi { 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 2d446490ec..9e02287ff0 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 @@ -8,7 +8,7 @@ import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.redux.global.CryptoCurrencyName -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder @@ -28,6 +28,8 @@ class MercuryoService( private val blockchainsAvailableToBuy = mutableListOf() private val tokensAvailableToBy = mutableMapOf>() + override fun featureIsSwitchedOn(): Boolean = true + override suspend fun update() { when (val result = performRequest { api.currencies(apiVersion) }) { is Result.Success -> { @@ -130,6 +132,7 @@ class MercuryoService( private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) { "BNB" -> Blockchain.BSC "ETH" -> Blockchain.Ethereum + "ADA" -> Blockchain.CardanoShelley else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() } } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 161cfc568a..00db516c5b 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -9,7 +9,7 @@ import com.tangem.domain.common.extensions.withIOContext import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName -import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder @@ -29,6 +29,8 @@ class MoonPayService( private var status: MoonPayStatus? = null + override fun featureIsSwitchedOn(): Boolean = true + override suspend fun update() { withIOContext { performRequest { 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 f676467852..aeab14497b 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -25,6 +25,10 @@ class PreferencesStorage(applicationContext: Application) { fiatCurrenciesPrefStorage.migrate() } + var chatFirstLaunchTime: Long? + get() = preferences.getLong(CHAT_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L } + set(value) = preferences.edit { putLong(CHAT_FIRST_LAUNCH_KEY, value ?: 0) } + fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1) @Deprecated("Use UsedCardsPrefStorage instead") @@ -53,20 +57,12 @@ class PreferencesStorage(applicationContext: Application) { preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) } } - fun wasRestoreFundsWarningClosed(): Boolean { - return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false) - } - - fun saveRestoreFundsWarningClosed() { - preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) } - } - 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 RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed" + private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" } } diff --git a/app/src/main/res/drawable/currency0.png b/app/src/main/res/drawable/currency0.png new file mode 100644 index 0000000000..0d293ec8bb Binary files /dev/null and b/app/src/main/res/drawable/currency0.png differ diff --git a/app/src/main/res/drawable/currency1.png b/app/src/main/res/drawable/currency1.png new file mode 100644 index 0000000000..37144efffe Binary files /dev/null and b/app/src/main/res/drawable/currency1.png differ diff --git a/app/src/main/res/drawable/currency2.png b/app/src/main/res/drawable/currency2.png new file mode 100644 index 0000000000..bd75790221 Binary files /dev/null and b/app/src/main/res/drawable/currency2.png differ diff --git a/app/src/main/res/drawable/currency3.png b/app/src/main/res/drawable/currency3.png new file mode 100644 index 0000000000..c517578807 Binary files /dev/null and b/app/src/main/res/drawable/currency3.png differ diff --git a/app/src/main/res/drawable/currency4.png b/app/src/main/res/drawable/currency4.png new file mode 100644 index 0000000000..9a39d5651d Binary files /dev/null and b/app/src/main/res/drawable/currency4.png differ diff --git a/app/src/main/res/drawable/dapps0.png b/app/src/main/res/drawable/dapps0.png new file mode 100644 index 0000000000..6152b1215b Binary files /dev/null and b/app/src/main/res/drawable/dapps0.png differ diff --git a/app/src/main/res/drawable/dapps1.png b/app/src/main/res/drawable/dapps1.png new file mode 100644 index 0000000000..2d4effaf8b Binary files /dev/null and b/app/src/main/res/drawable/dapps1.png differ diff --git a/app/src/main/res/drawable/dapps2.png b/app/src/main/res/drawable/dapps2.png new file mode 100644 index 0000000000..f7643c4ec7 Binary files /dev/null and b/app/src/main/res/drawable/dapps2.png differ diff --git a/app/src/main/res/drawable/dapps3.png b/app/src/main/res/drawable/dapps3.png new file mode 100644 index 0000000000..9e0110179b Binary files /dev/null and b/app/src/main/res/drawable/dapps3.png differ diff --git a/app/src/main/res/drawable/dapps4.png b/app/src/main/res/drawable/dapps4.png new file mode 100644 index 0000000000..4eae3c0985 Binary files /dev/null and b/app/src/main/res/drawable/dapps4.png differ diff --git a/app/src/main/res/drawable/dapps5.png b/app/src/main/res/drawable/dapps5.png new file mode 100644 index 0000000000..0c581624c7 Binary files /dev/null and b/app/src/main/res/drawable/dapps5.png differ diff --git a/app/src/main/res/drawable/ic_accepted.xml b/app/src/main/res/drawable/ic_accepted.xml new file mode 100644 index 0000000000..b94c8e5cf4 --- /dev/null +++ b/app/src/main/res/drawable/ic_accepted.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_alert.xml b/app/src/main/res/drawable/ic_alert.xml new file mode 100644 index 0000000000..befebf5833 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000000..f42551a533 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml new file mode 100644 index 0000000000..b377ea3f2a --- /dev/null +++ b/app/src/main/res/drawable/ic_backup.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_card_settings.xml b/app/src/main/res/drawable/ic_card_settings.xml new file mode 100644 index 0000000000..c9ac2c388d --- /dev/null +++ b/app/src/main/res/drawable/ic_card_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_cards.xml b/app/src/main/res/drawable/ic_cards.xml new file mode 100644 index 0000000000..9b226dff8e --- /dev/null +++ b/app/src/main/res/drawable/ic_cards.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chat.xml b/app/src/main/res/drawable/ic_chat.xml new file mode 100644 index 0000000000..465c9f741d --- /dev/null +++ b/app/src/main/res/drawable/ic_chat.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron.xml b/app/src/main/res/drawable/ic_chevron.xml new file mode 100644 index 0000000000..31e861dd95 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml index 16d6d37dd9..d276f8584c 100644 --- a/app/src/main/res/drawable/ic_close.xml +++ b/app/src/main/res/drawable/ic_close.xml @@ -1,10 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_comment.xml b/app/src/main/res/drawable/ic_comment.xml new file mode 100644 index 0000000000..53e56127b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_comment.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_currency.xml b/app/src/main/res/drawable/ic_currency.xml new file mode 100644 index 0000000000..7508a16997 --- /dev/null +++ b/app/src/main/res/drawable/ic_currency.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_facebook.xml b/app/src/main/res/drawable/ic_facebook.xml new file mode 100644 index 0000000000..9343c46fe7 --- /dev/null +++ b/app/src/main/res/drawable/ic_facebook.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000000..e2c70a78fa --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 0000000000..57bbd79618 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_gnosis_no_color.xml b/app/src/main/res/drawable/ic_gnosis_no_color.xml new file mode 100644 index 0000000000..a8d4069093 --- /dev/null +++ b/app/src/main/res/drawable/ic_gnosis_no_color.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_gnosis_round.xml b/app/src/main/res/drawable/ic_gnosis_round.xml new file mode 100644 index 0000000000..8734b7177b --- /dev/null +++ b/app/src/main/res/drawable/ic_gnosis_round.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_instagram.xml b/app/src/main/res/drawable/ic_instagram.xml new file mode 100644 index 0000000000..844639d9e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_instagram.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_linkedin.xml b/app/src/main/res/drawable/ic_linkedin.xml new file mode 100644 index 0000000000..36600f605d --- /dev/null +++ b/app/src/main/res/drawable/ic_linkedin.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000000..51ac9d64af --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000000..2ea52e3dbc --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_cards.xml b/app/src/main/res/drawable/ic_more_cards.xml new file mode 100644 index 0000000000..0cc559021b --- /dev/null +++ b/app/src/main/res/drawable/ic_more_cards.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_overlay.xml b/app/src/main/res/drawable/ic_overlay.xml index 586397f855..5487da88f8 100644 --- a/app/src/main/res/drawable/ic_overlay.xml +++ b/app/src/main/res/drawable/ic_overlay.xml @@ -4,10 +4,18 @@ - - - + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..e8b4a25be1 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_reset.xml b/app/src/main/res/drawable/ic_reset.xml new file mode 100644 index 0000000000..8c0845dbb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_reset.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_reset_background.xml b/app/src/main/res/drawable/ic_reset_background.xml new file mode 100644 index 0000000000..8ce3137945 --- /dev/null +++ b/app/src/main/res/drawable/ic_reset_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml index 476424f743..1e4cd949d6 100644 --- a/app/src/main/res/drawable/ic_scan.xml +++ b/app/src/main/res/drawable/ic_scan.xml @@ -1,16 +1,8 @@ - - - - + + + + + diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml new file mode 100644 index 0000000000..ba3c214505 --- /dev/null +++ b/app/src/main/res/drawable/ic_security.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 41a82ede8b..46fe7fa730 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -1,10 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_tangem.xml b/app/src/main/res/drawable/ic_tangem.xml new file mode 100644 index 0000000000..5140d0444c --- /dev/null +++ b/app/src/main/res/drawable/ic_tangem.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml new file mode 100644 index 0000000000..5d2351d73e --- /dev/null +++ b/app/src/main/res/drawable/ic_telegram.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_text.xml b/app/src/main/res/drawable/ic_text.xml new file mode 100644 index 0000000000..3e1f8a08a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_text.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_trash.xml b/app/src/main/res/drawable/ic_trash.xml new file mode 100644 index 0000000000..38e732373b --- /dev/null +++ b/app/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml new file mode 100644 index 0000000000..de09724525 --- /dev/null +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_unticked.xml b/app/src/main/res/drawable/ic_unticked.xml new file mode 100644 index 0000000000..8eb2fe34df --- /dev/null +++ b/app/src/main/res/drawable/ic_unticked.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_walletconnect.xml b/app/src/main/res/drawable/ic_walletconnect.xml new file mode 100644 index 0000000000..925a858a8a --- /dev/null +++ b/app/src/main/res/drawable/ic_walletconnect.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_youtube.xml b/app/src/main/res/drawable/ic_youtube.xml new file mode 100644 index 0000000000..81eb8d7e8c --- /dev/null +++ b/app/src/main/res/drawable/ic_youtube.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/img_warning_triangle_24.xml b/app/src/main/res/drawable/img_warning_triangle_24.xml new file mode 100644 index 0000000000..c54d7d8f8b --- /dev/null +++ b/app/src/main/res/drawable/img_warning_triangle_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/shape_badge_custom_currency.xml b/app/src/main/res/drawable/shape_badge_custom_currency.xml index 2c3b7f7bd4..cfd740103d 100644 --- a/app/src/main/res/drawable/shape_badge_custom_currency.xml +++ b/app/src/main/res/drawable/shape_badge_custom_currency.xml @@ -7,7 +7,7 @@ android:height="12dp" /> diff --git a/app/src/main/res/drawable/shape_rectangle_rounded_8.xml b/app/src/main/res/drawable/shape_rectangle_rounded_8.xml index a788f39093..4f20cb0e64 100644 --- a/app/src/main/res/drawable/shape_rectangle_rounded_8.xml +++ b/app/src/main/res/drawable/shape_rectangle_rounded_8.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/font/roboto.ttf b/app/src/main/res/font/roboto.ttf new file mode 100644 index 0000000000..ddf4bfacb3 Binary files /dev/null and b/app/src/main/res/font/roboto.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000000..ac0f908b9c Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/layout/card_total_balance.xml b/app/src/main/res/layout/card_total_balance.xml deleted file mode 100644 index 1f97649e18..0000000000 --- a/app/src/main/res/layout/card_total_balance.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml deleted file mode 100644 index 58ac038846..0000000000 --- a/app/src/main/res/layout/fragment_details.xml +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details_security.xml b/app/src/main/res/layout/fragment_details_security.xml deleted file mode 100644 index 25d17cf8cc..0000000000 --- a/app/src/main/res/layout/fragment_details_security.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index 7a95116cd8..0000000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml index 82b94a5def..faa2c0c829 100644 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ b/app/src/main/res/layout/fragment_onboarding_main.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" android:background="@color/backgroundWhite" android:clipChildren="false" + android:clipToPadding="false" android:fitsSystemWindows="true" android:orientation="vertical"> diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index faf0d33f9f..d5daad21ea 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -18,14 +18,14 @@ app:liftOnScroll="true"> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:menu="@menu/wallet" + app:navigationIcon="@drawable/ic_wallet_24" + app:navigationIconTint="@color/darkGray6" + app:title="@string/wallet_title" + app:titleCentered="true" /> @@ -43,43 +43,44 @@ android:overScrollMode="never"> + android:id="@+id/cl_wallet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:paddingBottom="92dp"> + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/card_placeholder_black" /> + + + app:layout_constraintTop_toBottomOf="@id/l_wallet_backup_warning" + app:layout_goneMarginTop="16dp" /> + app:layout_constraintTop_toBottomOf="@id/l_card_total_balance" + app:layout_goneMarginTop="12dp" /> + - - - + app:cornerRadius="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/rv_multiwallet" + app:layout_constraintVertical_bias="0.0" + tools:layout_editor_absoluteX="16dp" /> - - - + diff --git a/app/src/main/res/layout/item_currency_wallet.xml b/app/src/main/res/layout/item_currency_wallet.xml index de692cfa83..0c6ef1fa3f 100644 --- a/app/src/main/res/layout/item_currency_wallet.xml +++ b/app/src/main/res/layout/item_currency_wallet.xml @@ -6,75 +6,52 @@ android:layout_height="wrap_content"> + android:id="@+id/card_wallet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp" + android:background="@android:color/white" + android:elevation="3dp"> + + - - - - - - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + layout="@layout/item_currency_wallet_content" + android:id="@+id/l_content" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginStart="8dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/iv_currency" + app:layout_constraintEnd_toEndOf="parent" + tools:visibility="visible" /> + layout="@layout/item_currency_wallet_shimmer" + android:id="@+id/l_shimmer" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginStart="8dp" + app:layout_constraintStart_toEndOf="@id/iv_currency" + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/layout_balance_wallet_details.xml b/app/src/main/res/layout/layout_balance_wallet_details.xml index 781b8f5145..0ae3a9e463 100644 --- a/app/src/main/res/layout/layout_balance_wallet_details.xml +++ b/app/src/main/res/layout/layout_balance_wallet_details.xml @@ -39,28 +39,13 @@ app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" tools:text="0.43 BTC" /> - - - + android:layout_margin="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/layout_card_total_balance.xml b/app/src/main/res/layout/layout_card_total_balance.xml new file mode 100644 index 0000000000..fe3484f373 --- /dev/null +++ b/app/src/main/res/layout/layout_card_total_balance.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_single_wallet_balance.xml b/app/src/main/res/layout/layout_single_wallet_balance.xml new file mode 100644 index 0000000000..119baed8cf --- /dev/null +++ b/app/src/main/res/layout/layout_single_wallet_balance.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_wallet_backup_warning.xml b/app/src/main/res/layout/layout_wallet_backup_warning.xml new file mode 100644 index 0000000000..119ab12734 --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_backup_warning.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_wallet_long_buttons.xml b/app/src/main/res/layout/layout_wallet_long_buttons.xml index f670b7fd5d..5fc11bef41 100644 --- a/app/src/main/res/layout/layout_wallet_long_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_long_buttons.xml @@ -17,11 +17,8 @@ android:id="@+id/btn_confirm_long" style="@style/TapButtonWithIcon" android:layout_width="0dp" - android:layout_marginTop="30dp" - android:layout_marginEnd="16dp" - android:layout_marginBottom="8dp" android:text="@string/wallet_button_send" - app:icon="@drawable/ic_send" + app:icon="@drawable/ic_send" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/guideline" diff --git a/app/src/main/res/layout/layout_wallet_short_buttons.xml b/app/src/main/res/layout/layout_wallet_short_buttons.xml index ed60601395..e235511583 100644 --- a/app/src/main/res/layout/layout_wallet_short_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_short_buttons.xml @@ -6,38 +6,32 @@ + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:iconGravity="textEnd" + android:textAlignment="center" + app:layout_constraintVertical_bias="1" /> + app:layout_constraintStart_toEndOf="@id/btn_trade" + app:layout_constraintVertical_bias="1" /> diff --git a/app/src/main/res/layout/view_currency_icon.xml b/app/src/main/res/layout/view_currency_icon.xml new file mode 100644 index 0000000000..801ac76c97 --- /dev/null +++ b/app/src/main/res/layout/view_currency_icon.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_wallet_details_buttons_row.xml b/app/src/main/res/layout/view_wallet_details_buttons_row.xml index bb5bd190b0..0a6a51bfdd 100644 --- a/app/src/main/res/layout/view_wallet_details_buttons_row.xml +++ b/app/src/main/res/layout/view_wallet_details_buttons_row.xml @@ -1,50 +1,56 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:orientation="horizontal" + tools:parentTag="android.widget.LinearLayout"> - + + - - + + + + android:id="@+id/btn_send" + style="@style/TapButtonWithIcon" + android:layout_width="0dp" + android:layout_weight="1" + android:text="@string/wallet_button_send" + app:icon="@drawable/ic_send" /> diff --git a/app/src/main/res/menu/wallet_details.xml b/app/src/main/res/menu/wallet_details.xml index 08ba8df389..d49dd52b30 100644 --- a/app/src/main/res/menu/wallet_details.xml +++ b/app/src/main/res/menu/wallet_details.xml @@ -3,6 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3e7c059c3a..cf8a46c64c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -6,6 +6,7 @@ Erledigt Abbrechen Akzeptieren + Entfernen Der Kamerazugriff wurde verweigert Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an @@ -34,10 +35,10 @@ Diese Karte wird nicht unterstützt Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App Die Transaktion läuft… - Senden\u0020 - an %s - Erhalten\u0020 - von %s + Senden + an %s + Erhalten + von %s Adresse erkunden PayString erstellen %s Wallet @@ -58,11 +59,6 @@ Gebühr Gesamt Höchstbetrag - Ungültige Adresse - Ungültiger Betrag - Der Betrag geht über die Bilanz hinaus - Der Gesamtbetrag geht über die Bilanz hinaus - Die Gebühr geht über die Bilanz hinaus Niedrig Normal Priorität @@ -72,7 +68,6 @@ %s %s und %s %s werden gesendet Bilanz: %s %s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert - Um ein Konto zu erstellen, senden Sie 1+ XLM an diese Adresse Details KartenID Emittent @@ -164,21 +159,12 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Nutzungsbedingungen Keine Internetverbindung - Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %1$s %2$s + Gebühr oder mehr sein - Erhalt der Gebühr fehlgeschlagen - - Unbekannter Fehler Verifikation der PayString ist fehlgeschlagen PayString wird von der Blockchain nicht unterstützt PayString ist nicht registriert PayString-Anfrage ist fehlgeschlagen Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein - Nicht genug Geld - Interner Fehler der Blockchain - Falsche Gebühr - Minimaler Betrag ist %s - Restbestand zu klein Die Adresse wurde erfolgreich kopiert @@ -191,8 +177,4 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Ihr Kontostand in dieser Brieftasche ist nicht Null, oder Sie haben unbestätigte Transaktionen. Es ist unmöglich die Funktion der Brieftasche zu löschen. Dies ist die derzeit aktive Wahl - Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden - Um %s XTZ reduzieren - Nein, alles senden - diff --git a/app/src/main/res/values-de/strings_blockchain.xml b/app/src/main/res/values-de/strings_blockchain.xml new file mode 100644 index 0000000000..2f52667c19 --- /dev/null +++ b/app/src/main/res/values-de/strings_blockchain.xml @@ -0,0 +1,20 @@ + + + Nicht genug Geld + Interner Fehler der Blockchain + Falsche Gebühr + Minimaler Betrag ist %s + Restbestand zu klein + Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s %s + Gebühr oder mehr sein + Um ein Konto zu erstellen, senden Sie 1+ XLM an diese Adresse + Erhalt der Gebühr fehlgeschlagen + Unbekannter Fehler + Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden + Um %s XTZ reduzieren + Nein, alles senden + Ungültige Adresse + Ungültiger Betrag + Der Betrag geht über die Bilanz hinaus + Der Gesamtbetrag geht über die Bilanz hinaus + Die Gebühr geht über die Bilanz hinaus + \ 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 de09eb54fd..96c0876eff 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -45,5 +47,52 @@ If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom + Notice + Attention + The server is not available, please try again later + Hide token + Hide %s + Hide + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. + Unable to hide %s + The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list. + + %s network not found. Please, add it first and try again. + This network is not supported. Please select another network. + %s network + + Card Settings + Security Mode + Selected application protection method + Change access code + Access code will be changed on this card only + Get your card ready! + Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan Card + App settings + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem app + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Removing the saved card deletes all the saved wallets and their access codes. + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + WalletConnect + Connect to Dapps + + Link More Cards + You can synchronize up to three cards into one wallet. It can only be done once. + + Privacy policy + + Reset to factory settings + This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + I understand that after performing this action, I will no longer have access to the current wallet + Reset the card + Reset to Factory Settings + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + + Select network \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 069163f054..d39650f3b7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,112 +1,107 @@ - Sauvegarder les modifications - Alerte - Réessayer - Exécuté - Annuler + Sauvegarder les modifications + Alerte + Réessayer + Exécuté + Annuler J\'accepte + Supprimer - L\'accès à la caméra est refusé - Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité - Paramètres + L\'accès à la caméra est refusé + Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité + Paramètres Bienvenue à Tangem. Avez-vous une de nos cartes - Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer. - Oui! Scannez-la. - Magasin + Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer. + Oui! Scannez-la. + Magasin Préparez-vous à appuyer votre carte au dos de votre téléphone. - Appuyez - Scannez la carte - Scanner - Envoyer - Créer un portefeuille + Appuyez + Scannez la carte + Scanner + Envoyer + Créer un portefeuille Tangem - Choisir une variante de portefeuille - Jeton d\'authentification - Jetons d\'authentification - Solde confirmé - Solde est en cours de téléchargement … - Blockchain est inaccessible - Compte n\'est pas créé - Téléchargez %s +%s pour créer un compte - Carte vide - Créez un portefeuille pour commencer à utiliser votre carte Tangem - Cette carte n\'est pas prise en charge - Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente + Choisir une variante de portefeuille + Jeton d\'authentification + Jetons d\'authentification + Solde confirmé + Solde est en cours de téléchargement … + Blockchain est inaccessible + Compte n\'est pas créé + Téléchargez %s +%s pour créer un compte + Carte vide + Créez un portefeuille pour commencer à utiliser votre carte Tangem + Cette carte n\'est pas prise en charge + Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente Transaction en cours… - Envoi\u0020 - jusqu\'à %s - Réception \u0020 - de %s - Explorer l\'adresse + Envoi + jusqu\'à %s + Réception + de %s + Explorer l\'adresse Créer PayString Portefeuille %s Créer PayString Carte: %s Dénomination PayString $payid.tangem.com - Créer + Créer Votre PayString est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN. - PayString a été créé avec succès et copié dans le presse-papiers - Mauvaise réponse lors de la création de PayString + PayString a été créé avec succès et copié dans le presse-papiers + Mauvaise réponse lors de la création de PayString Ce PayString existe déjà. Essayez une autre variante. Adresse ou PayString Adresse - Envoyer - Commissions du réseau - Somme - Commissions - Total - Somme maximale - Adresse incorrecte - Somme incorrecte - Le montant dépasse le solde - Le total dépasse le solde - Les commissions dépassent le solde - Bas - Normal - Priorité - Inclure les commissions + Envoyer + Commissions du réseau + Somme + Commissions + Total + Somme maximale + Bas + Normal + Priorité + Inclure les commissions Sera envoyé %s %s ≈ %s (incl. les commissions : %s) Sera envoyé %s %s et %s %s Solde : %s %s - La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps - Pour créer un compte, envoyez 1+ XLM à cette adresse - Référénces - ID de la carte - Emetteur - Signé - %s hashes - Monnaie de l\'application - Paramètres - Carte - Valider la carte - Gestion de la sécurité - Annuler le portefeuille + La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + Référénces + ID de la carte + Emetteur + Signé + %s hashes + Monnaie de l\'application + Paramètres + Carte + Valider la carte + Gestion de la sécurité + Annuler le portefeuille Cette action est irréversible. Si, après avoir annulé le portefeuille, quelqu\'un lui envoie des fonds, vous ne pourrez pas les retirer. - Si vous oubliez le code, vous perdrez la possibilité d\'utiliser la carte. Si vous perdez le code, il sera impossible de le restaurer ou modifier. - Gestion de la sécurité + Si vous oubliez le code, vous perdrez la possibilité d\'utiliser la carte. Si vous perdez le code, il sera impossible de le restaurer ou modifier. + Gestion de la sécurité Tenez la carte fermement Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. - Mot de passe + Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. - Code d\'accès - Vous devrez entrer le mot de passe correct avant de scanner la carte - Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de l\'extraction + Code d\'accès + Vous devrez entrer le mot de passe correct avant de scanner la carte + Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de l\'extraction Cette carte a déjà été rechargée et a signé des transactions avant. Envisagez la possibilité de retirer tous les fonds immédiatement si vous avez reçu cette carte d\'une source non fiable. Attention Cette carte n\'est pas conçue pour fonctionner avec Tangem - La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement - Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. - Touchez pour signer - Appuyez votre carte au téléphone comme indiqué ci-dessus - Touchez, pour annuler un portefeuille - Touchez, pour créer un portefeuille - Touchez, pour modifier le code d\'accès - Touchez, pour modifier le mot de passe - PayString incorrect + La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement + Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. + Touchez pour signer + Appuyez votre carte au téléphone comme indiqué ci-dessus + Touchez, pour annuler un portefeuille + Touchez, pour créer un portefeuille + Touchez, pour modifier le code d\'accès + Touchez, pour modifier le mot de passe + PayString incorrect @@ -159,23 +154,14 @@ Avertissement \n\nCette avertissement a été modifié, pour la dernière fois, le 1 octobre 2020. - Conditions d\'utilisation + Conditions d\'utilisation Pas de connexion internet - Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %1$s %2$s + commissions ou plus - Échec de réception des commissions - - Erreur inconnue La vérification de PayString a échoué PayString non pris en charge par la blockchain PayString non enregistré La demande de PayString a échoué L\'adresse est la même que celle de votre portefeuille - Solde insuffisant - Erreur interne de la blockchain - Commission non valide - Le montant minimal est de %ы - Le reste est trop petit L\'adresse a été copiée avec succès Saisissez d\'abord le PayString requis Posez pour scanner @@ -183,8 +169,5 @@ Avertissement Votre solde sur ce portefeuille n\'est pas nul, ou vous avez des transactions non confirmées. Impossible de supprimer la fonction du portefeuille Cette option est déjà active - Pour ne pas payer une commission élevée la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ - Réduire de% s XTZ - Non, envoyer toute la somme diff --git a/app/src/main/res/values-fr/strings_blockchain.xml b/app/src/main/res/values-fr/strings_blockchain.xml new file mode 100644 index 0000000000..f6e2cdef63 --- /dev/null +++ b/app/src/main/res/values-fr/strings_blockchain.xml @@ -0,0 +1,20 @@ + + + Solde insuffisant + Erreur interne de la blockchain + Commission non valide + Le montant minimal est de %s + Le reste est trop petit + Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s %s + commissions ou plus + Pour créer un compte, envoyez 1+ XLM à cette adresse + Échec de réception des commissions + Erreur inconnue + Adresse incorrecte + Somme incorrecte + Le montant dépasse le solde + Le total dépasse le solde + Les commissions dépassent le solde + Pour ne pas payer une commission élevée la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ + Réduire de %s XTZ + Non, envoyer toute la somme + \ 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 de09eb54fd..3fe6eb9f17 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -45,5 +47,52 @@ 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 + Hide token + Hide %s + Hide + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. + Unable to hide %s + The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list. + + %s network not found. Please, add it first and try again. + This network is not supported. Please select another network. + %s network + + Card Settings + Security Mode + Selected application protection method + Change access code + Access code will be changed on this card only + Get your card ready! + Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan Card + App settings + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem app + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Removing the saved card deletes all the saved wallets and their access codes. + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + WalletConnect + Connect to Dapps + + Link More Cards + You can synchronize up to three cards into one wallet. It can only be done once. + + Privacy policy + + Reset to factory settings + This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + I understand that after performing this action, I will no longer have access to the current wallet + Reset the card + Reset to Factory Settings + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + + Select network \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7ad1cddc70..7eba948ed7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -6,6 +6,7 @@ Riprova Fatto Annulla + Rimuovere Accesso alla videocamera negato Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy Impostazioni @@ -33,10 +34,10 @@ Questa carta non è supportata La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta Transazione in corso… - Invia\u0020 - su %s - Ricevi\u0020 - da %s + Invia + su %s + Ricevi + da %s Cerca indirizzo Crea PayString portafoglio %s @@ -57,10 +58,7 @@ Commissione Totale Importo totale - Indirizzo non valido - Importo non valido - Il totale supera il saldo - La commissione supera il saldo + Insufficiente Normale Prioritario @@ -70,7 +68,6 @@ Sarà inviato %1$s %2$s e %1$s %2$s Saldo: %1$s %2$s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo - Per creare un account, invia 1+ XLM a questo indirizzo Requisiti ID carta Emittente @@ -158,26 +155,16 @@ Salvo quanto diversamente previsto dalla legge, i proprietari o i contributori d Questa nota legale è stata modificata l\'ultima volta 01.10.2020. - - Per creare un account, invia %1$s %2$s a questo indirizzo - Impossibile ottenere la commissione Accetta - L\'importo supera il saldo Termini del servizio Nessuna connessione a Internet - Errore sconosciuto Verifica del PayString fallita PayString non supportato dalla blockchain PayString non registrato Richiesta PayString fallita L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio - Saldo insufficiente - Errore interno della blockchain - Commissione non valida - L\'importo minimo è di %s - L\'importo residuo è molto basso L\'indirizzo è stato copiato con successo @@ -190,8 +177,4 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Il tuo saldo su questo portafoglio non è pari a zero o hai transazioni non confermate. Non è possibile eliminare la funzione portafoglio. E\' l\'opzione attualmente attiva - Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ - Riduci di %s XTZ - No, invia l\'intero importo - diff --git a/app/src/main/res/values-it/strings_blockchain.xml b/app/src/main/res/values-it/strings_blockchain.xml new file mode 100644 index 0000000000..cd0f690a23 --- /dev/null +++ b/app/src/main/res/values-it/strings_blockchain.xml @@ -0,0 +1,20 @@ + + + Saldo insufficiente + Errore interno della blockchain + Commissione non valida + L\'importo minimo è di %s + L\'importo residuo è molto basso + Errore sconosciuto + L\'importo supera il saldo + Per creare un account, invia %s %s a questo indirizzo + Impossibile ottenere la commissione + Per creare un account, invia 1+ XLM a questo indirizzo + Indirizzo non valido + Importo non valido + Il totale supera il saldo + La commissione supera il saldo + Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ + Riduci di %s XTZ + No, invia l\'intero importo + \ 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 de09eb54fd..afe988cb58 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -45,5 +47,53 @@ 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 + Hide token + Hide %s + Hide + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. + Unable to hide %s + The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list. + + %s network not found. Please, add it first and try again. + This network is not supported. Please select another network. + + %s network + + Card Settings + Security Mode + Selected application protection method + Change access code + Access code will be changed on this card only + Get your card ready! + Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan Card + App settings + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem app + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Removing the saved card deletes all the saved wallets and their access codes. + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + WalletConnect + Connect to Dapps + + Link More Cards + You can synchronize up to three cards into one wallet. It can only be done once. + + Privacy policy + + Reset to factory settings + This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + I understand that after performing this action, I will no longer have access to the current wallet + Reset the card + Reset to Factory Settings + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + + Select network \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000000..9be291c700 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 3df2976282..d36add64ec 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,6 +7,7 @@ Готово Отмена Принять + Удалить %s Доступ к камере запрещен Вы не предоставили доступ к своей камере, измените настройки конфиденциальности @@ -37,9 +38,9 @@ Транзакция в процессе… до %s от %s - Получение\u0020 - Отправка\u0020 - Посмотреть адрес + Получение + Отправка + Проверить адрес Создать PayString %s кошелек Создать PayString @@ -59,11 +60,6 @@ Комиссия Всего Максимальная сумма - Неверный адрес - Недопустимая сумма - Сумма превышает баланс - Общая сумма превышает баланс - Комиссия превышает баланс Низкий Нормальный Приоритетный @@ -107,30 +103,19 @@ Приложите, чтобы изменить пароль Неверный PayString Условия использования - Не удалось получить комиссию - Целевая учетная запись не создана. Сумма для отправки должна быть %1$s %2$s + плата за создание или больше Нет соединения с интернетом - Неизвестная ошибка Ошибка проверки PayString PayString не поддерживается блокчейном PayString не зарегистрирован Не удалось выполнить запрос PayString Адрес совпадает с адресом кошелька - Недостаточный баланс - Внутренняя ошибка блокчейна - Неверная комиссия - Минимальная сумма: %s - Сдача слишком мала - Для создания учетной записи отправьте 1+ XLM на этот адрес + Адрес скопирован в буфер обмена Сначала введите желаемую PayString Приложите, чтобы отсканировать Приложите карту Ваш баланс на этом кошельке не равен нулю, или у Вас есть неподтвержденные транзакции Это текущая активная опция - Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ - Уменьшить на %s XTZ - Нет, отправить все Ошибка сканирования карты. Попробуй снова. Купить Продать @@ -175,7 +160,7 @@ Не удалось отправить электронное письмо Причина: %s Не могу отправить транзакцию - Причина: %s. Хотите отправить отзыв? + Причина: %s У Вас возникли трудности со сканированием вашей карты? Пожалуйста, попробуйте приложить карту точно так, как показано на анимации, или обратитесь в поддержку Отмена @@ -197,7 +182,6 @@ Добавить пользовательский Популярные токены Добавлен - Удалить токен Значок токена Добавить токен Адрес контракта @@ -324,7 +308,6 @@ Активный. Количество резервных карт: %d Связано. Количество резервных карт: %d Нет резервной копии - Создать резервную копию У Вас есть прерванное резервное копирование. Хотите возобновить? Да, возобновить Отказаться @@ -340,7 +323,6 @@ Подготовьте резервную карту с номером %s Подготовьте основную карту с номером %s Сброс до заводских настроек - Это действие необратимо. Если после сброса карты кто-то отправит на нее средства, то Вы не сможете их вывести. Код доступа доступен только для карт с резервной копией Сеть Solana взымает арендную плату в размере %s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %s, чтобы использовать его бесплатно. %1s (%2s) diff --git a/app/src/main/res/values-ru/strings_blockchain.xml b/app/src/main/res/values-ru/strings_blockchain.xml new file mode 100644 index 0000000000..98b7ad7194 --- /dev/null +++ b/app/src/main/res/values-ru/strings_blockchain.xml @@ -0,0 +1,20 @@ + + + Недостаточный баланс + Внутренняя ошибка блокчейна + Неверная комиссия + Минимальная сумма: %s + Сдача слишком мала + Для создания учетной записи отправьте 1+ XLM на этот адрес + Не удалось получить комиссию + Целевая учетная запись не создана. Сумма для отправки должна быть %1$s %2$s + плата за создание или больше + Неизвестная ошибка + Неверный адрес + Недопустимая сумма + Сумма превышает баланс + Общая сумма превышает баланс + Комиссия превышает баланс + Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ + Уменьшить на %s XTZ + Нет, отправить все + \ 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 cd706ff8af..359b5c0488 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -23,7 +23,9 @@ Революционный аппаратный кошелек Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Все ключи в безопасности - До трех карт с одним кошельком + До + трех карт + с одним кошельком Тысячи криптовалют Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте Поддержка DeFi @@ -45,6 +47,9 @@ Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства Пользовательский + Уведомление + Внимание + Сервер недоступен, повторите попытку позднее Баланс В сумме учтены не все монеты @@ -54,11 +59,14 @@ Скрыть токен Скрыть %s Скрыть - Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно. + Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. Невозможно скрыть %s - Токен %s является основной валютой в сети %s и не может быть скрыт, до тех пор пока у вас в списке есть другие токены этой сети. + + Токен %s является основной валютой в сети %s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. + Сеть не поддерживается. Пожалуйста, выберите другую сеть. + Сеть %s Бэкап кошелька не был произведен @@ -67,4 +75,41 @@ Карты банков РФ в данный момент не принимаются У вас есть карта банка другой страны или платежной системы UnionPay? Да + + Чат + Tangem Bot + + Настройки карты + Тип безопасности + Выбранный способ защиты приложения + Смена кода доступа + Код доступа будет изменен только на данной карте + Приготовьте свою карту + Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. + Сканировать + Настройки приложения + Включите биометрическую аутентификацию + Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem + Cохранение кошелька + Если эта опция включена, кошельки всех отсканированных карт будут привязываться к приложению и при открытии приложения будет необходимо пройти биометрическую аутентификацию. Подпись транзакций все так же потребует карту. + Сохранение кода доступа + Если эта опция включена, пароли от карты будут хранится на телефоне в зашифрованном виде и при работе с картой вместо пароля будет запрашиваться биометрическая аутентификация + При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены. + Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. + WalletConnect + Подключение к Dapps + + Добавить еще карты + Вы можете объединить до трех карт в одном кошельке. Это можно сделать только один раз. + + Privacy policy + + Сброс к заводским настройкам + Это действие приведет к полному удалению кошелька на этой карте. Кошелек невозможно будет восстановить или использовать данную карту для восстановления кода доступа + Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку + Сбросить карту + Сброс к заводским настройкам + Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать эту карту для восстановления кода доступа. + + Выберите сеть diff --git a/app/src/main/res/values-v29/zendesk_styles.xml b/app/src/main/res/values-v29/zendesk_styles.xml new file mode 100644 index 0000000000..628e83211a --- /dev/null +++ b/app/src/main/res/values-v29/zendesk_styles.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 70a942b055..b00c6603a8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -64,4 +64,82 @@ #CA0F03 #EFEFF0 + + #DE1010 + #007AFF + #000000 + #0029FF + #919191 + #656565 + #494949 + #3B3B3B + #303030 + #1E1E1E + #06311F + #ED7979 + #F5F5F5 + #EBEBEB + #D3D3D3 + #C9C9C9 + #B0B0B0 + #A3EBCC + #1ACE80 + #FFB71B + #FFFFFF + + + #000000 + #FFFFFF + #FFFFFF + #F5F5F5 + + + + #EBEBEB + #1ACE80 + #A3EBCC + #1E1E1E + #EBEBEB + + + #1ACE80 + #FFFFFF + #EBEBEB + + + + #EBEBEB + #F5F5F5 + + + + #1ACE80 + #FFB71B + #C9C9C9 + #B0B0B0 + #000000 + #FFFFFF + #656565 + #DE1010 + + + + #EBEBEB + #663B3B3B + #FFFFFF + + + + #1ACE80 + #FFB71B + #FFFFFF + #C9C9C9 + #1E1E1E + #FFFFFF + #656565 + #919191 + #DE1010 + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a05ac5fae..2c1ebffd92 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Done Cancel Accept + Delete %s Camera access denied @@ -44,8 +45,8 @@ Transaction is in progress… to %s from %s - Receiving\u0020 - Sending\u0020 + Receiving + Sending Explore address Create PayString @@ -69,11 +70,6 @@ Fee Total Maximum amount - Invalid address - Invalid amount - Amount Exceeds Balance - Total Exceeds Balance - Fee Exceeds Balance Low Normal Priority @@ -172,23 +168,14 @@ \nThis disclaimer was amended for the last time on October 1st, 2020. Terms of service - Failed to get fee - Target account is not created. Amount to send should be %1$s %2$s + fee or more to create - No internet connection + No internet connection - Unknown error PayString verification failed PayString unsupported by blockchain PayString not registered PayString request failed Address is the same as wallet address - Insufficient balance - Blockchain internal error - Invalid Fee - Minimum amount is %s - Change is too small - To create account send 1+ XLM to this address Address was copied to clipboard @@ -200,9 +187,4 @@ Your balance on this wallet is not zero, or you have unconfirmed transactions This is the currently active option - - - To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ - Reduce by %s XTZ - No, send all diff --git a/app/src/main/res/values/strings_blockchain.xml b/app/src/main/res/values/strings_blockchain.xml new file mode 100644 index 0000000000..35bc8cf334 --- /dev/null +++ b/app/src/main/res/values/strings_blockchain.xml @@ -0,0 +1,20 @@ + + + Insufficient balance + Blockchain internal error + Invalid Fee + Minimum amount is %s + Change is too small + To create account send 1+ XLM to this address + Failed to get fee + Target account is not created. Amount to send should be %s %s + fee or more to create + Unknown error + Invalid address + Invalid amount + Amount Exceeds Balance + Total Exceeds Balance + Fee Exceeds Balance + To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ + Reduce by %s XTZ + No, send all + \ 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 3a16ec6ea0..ca04743898 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -45,6 +47,9 @@ If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom + Notice + Attention + The server is not available, please try again later Total balance The amount does not include some of your funds @@ -54,11 +59,13 @@ Hide token Hide %s Hide - You are about to hide this token from the main screen. You can add it back anytime. + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Unable to hide %s The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list. %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 @@ -69,4 +76,42 @@ 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 + Change access code + Access code will be changed on this card only + Get your card ready! + Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan Card + App settings + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem app + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Removing the saved card deletes all the saved wallets and their access codes. + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + WalletConnect + Connect to Dapps + + Link More Cards + You can synchronize up to three cards into one wallet. It can only be done once. + + Privacy policy + + Reset to factory settings + This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + I understand that after performing this action, I will no longer have access to the current wallet + Reset the card + Reset to Factory Settings + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + + Select network diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 1f498055db..405518a258 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -55,7 +55,7 @@ Failed to send email Reason: %s Can\'t send a transaction - Reason: %s. Do you want to send feedback? + Reason: %s Are you having difficulty scanning your card? Please try to tap the card exactly as shown in the animation or request support. I\'m okay @@ -79,7 +79,6 @@ Custom tokens Popular tokens Added - Remove token Token icon Add Token @@ -132,7 +131,7 @@ There are only %s signatures available on this card. You must withdraw all of your funds. //Wallet Connect - Wallet Connect + WalletConnect Wallet Connect Sessions WalletConnect session opened with %s Ooops. No Sessions. @@ -284,7 +283,6 @@ Active. Backup cards count: %d Linked. Backup cards count: %d No backup - Create backup You have an interrupted backup. Do you want to resume? Yes, resume Discard @@ -300,7 +298,6 @@ Prepare the backup card with number %s Prepare the primary card with number %s Reset to factory settings - This action is irreversible. If, after resetting the card, someone sends funds to it, then you will not be able to withdraw them. Access code is available only for cards with a backup. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f36d1cd4bf..09cdff0b35 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -5,10 +5,6 @@ @color/colorPrimary @color/colorSecondary @color/darkGray6 - true - true - true - @null @color/menu_accent_color @style/Widget.AppTheme.CardView diff --git a/app/src/main/res/values/zendesk_styles.xml b/app/src/main/res/values/zendesk_styles.xml new file mode 100644 index 0000000000..ebb4fcab6b --- /dev/null +++ b/app/src/main/res/values/zendesk_styles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..2bbb4f525b --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + api.tangem.com + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 23b9e4d0bc..64f57c2248 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ allprojects { mavenLocal() maven { url "https://nexus.tangem-tech.com/repository/maven-releases/" } maven { url 'https://jitpack.io' } + maven { url 'https://zendesk.jfrog.io/zendesk/repo' } } } diff --git a/dependencies.gradle b/dependencies.gradle index d463106208..e1c383532f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,10 +1,13 @@ ext.versions = [ - kotlin : '1.6.10', - build_gradle: '7.1.3', + kotlin : '1.6.10', + build_gradle : '7.1.3', + tangem_card_sdk : 'develop-159', + tangem_blockchain_sdk: 'develop-105', + // tangem_blockchain_sdk: '0.0.1', ] ext.environmentConfig = [ - environment: "ENVIRONMENT", + environment : "ENVIRONMENT", testActionEnabled: "TEST_ACTION_ENABLED", logEnabled : "LOG_ENABLED", -] \ No newline at end of file +] diff --git a/domain/build.gradle b/domain/build.gradle index c2e5c807db..83c9fe6ff1 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -59,10 +59,9 @@ dependencies { implementation implementation(project(path: ':network')) implementation implementation(project(path: ':common')) - implementation 'com.tangem:blockchain:AND-1939_tron_fix_release-97' -// implementation 'com.tangem:blockchain:0.0.1' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154' + implementation "com.tangem:blockchain:${versions.tangem_blockchain_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}" // Kotlin coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' 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 644cc7525e..a11441c51c 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,18 @@ import java.util.* [REDACTED_AUTHOR] */ object TapWorkarounds { - 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) { null @@ -42,22 +40,19 @@ object TapWorkarounds { return false } - @Deprecated("Use ScanResponse.isTangemNote") - fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) - + fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId) || isSaltPay fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) - - fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[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" + "0035", ) private val excludedIssuers = listOf( 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 b69ffb1c49..8708f1a0a8 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 @@ -37,6 +37,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "tron" -> Blockchain.Tron "tron/test" -> Blockchain.TronTestnet "xrp", "ripple" -> Blockchain.XRP + "xdai" -> Blockchain.Gnosis else -> null } } @@ -78,6 +79,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.XRP -> "xrp" Blockchain.Tron -> "tron" Blockchain.TronTestnet -> "tron/test" + Blockchain.Gnosis -> "xdai" } } @@ -102,6 +104,7 @@ fun Blockchain.toCoinId(): String { Blockchain.XRP -> "ripple" Blockchain.Dogecoin -> "dogecoin" Blockchain.Tron, Blockchain.TronTestnet -> "tron" - else -> "unknown" + Blockchain.Gnosis -> "xdai" + Blockchain.Unknown -> "unknown" } } \ No newline at end of file diff --git a/network/build.gradle b/network/build.gradle index b90ff59980..e7fccceda6 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -12,7 +12,7 @@ dependencies { implementation implementation(project(path: ':common')) // Tangem sdk's - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" // Kotlin coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'