diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4fa62fd751 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV ANDROID_HOME=/opt/android-sdk +ENV BUNDLE_PATH=vendor/bundle +ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/34.0.0:$BUNDLE_PATH/bin +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 + +RUN apt-get update && apt-get install -y \ + openjdk-17-jdk \ + wget \ + unzip \ + curl \ + git \ + ruby \ + ruby-dev \ + build-essential \ + && apt-get clean + +RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \ + wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \ + unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \ + mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \ + rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \ + rm -f /tmp/cmdline-tools.zip && \ + yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \ + $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0" + +RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \ + unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \ + cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \ + rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip + +RUN gem install bundler:2.5.23 +RUN gem install fastlane -v 2.211.0 -N -V +RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V + +COPY Gemfile Gemfile.lock ./ +RUN bundle install --jobs=4 --retry=3 --verbose + +RUN bundle exec fastlane -v + +CMD ["bash"] diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7da64b0215..9c4e5fbd00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(deps.plugins.android.application) alias(deps.plugins.kotlin.android) @@ -25,6 +27,23 @@ android { androidResources { generateLocaleConfig = true } + signingConfigs { + getByName("debug") { + val keystorePath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore" + val propertiesPath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore.properties" + + val keystoreProperties = Properties() + file(propertiesPath) + .inputStream() + .use { keystoreProperties.load(it) } + + storeFile = file(keystorePath) + storePassword = keystoreProperties["store_password"] as String + keyAlias = keystoreProperties["key_alias"] as String + keyPassword = keystoreProperties["key_password"] as String + } + } + } configurations.all { @@ -75,6 +94,7 @@ dependencies { implementation(projects.domain.markets) implementation(projects.domain.manageTokens) implementation(projects.domain.onramp) + implementation(projects.domain.promo) implementation(projects.common) implementation(projects.common.routing) @@ -166,6 +186,7 @@ dependencies { implementation(deps.androidx.browser) implementation(deps.androidx.paging.runtime) implementation(deps.androidx.swipeRefreshLayout) + implementation(deps.androidx.fragment.compose) implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.common.java8) implementation(deps.lifecycle.viewModel.ktx) @@ -226,11 +247,12 @@ dependencies { implementation(deps.xmlShimmer) implementation(deps.viewBindingDelegate) implementation(deps.armadillo) - implementation(deps.mviCore.watcher) implementation(deps.kotlin.serialization) implementation(deps.reownCore) implementation(deps.reownWeb3) implementation(deps.prettyLogger) + implementation(deps.decompose.ext.compose) + implementation(deps.moshi.adapters) /** Testing libraries */ testImplementation(deps.test.coroutine) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fbb777f36c..1ebb3c801a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + @@ -10,11 +10,6 @@ - - - diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index f1aa2f2cbf..72d9d4b3c3 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit f1aa2f2cbf374ecba5da753d9f51c9df81749ef2 +Subproject commit 72d9d4b3c3f8e6604078798c4b5567327ed9b48a diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 67b96dd4a9..380e985b29 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -759,6 +759,16 @@ } ] }, + { + "id": "vanar-chain", + "name": "Vanar Chain", + "symbol": "VANRY", + "networks": [ + { + "networkId": "vanar-chain/test" + } + ] + }, { "id": "casper-network", "name": "Casper", diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt deleted file mode 100644 index 3ef77ac909..0000000000 --- a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap - -import android.content.Intent -import android.hardware.biometrics.BiometricManager -import android.os.Build -import android.provider.Settings -import androidx.activity.result.ActivityResultLauncher - -interface ActivityResultCaller { - val activityResultLauncher: ActivityResultLauncher? -} - -internal fun ActivityResultCaller.openSystemBiometrySettings() { - val settingsAction = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - Settings.ACTION_BIOMETRIC_ENROLL - } - else -> { - Settings.ACTION_SECURITY_SETTINGS - } - } - val intent = Intent(settingsAction).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - putExtra( - Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, - BiometricManager.Authenticators.BIOMETRIC_STRONG, - ) - } - } - - activityResultLauncher?.launch(intent) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 95b59844e2..328a4fe0f8 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -8,8 +8,10 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -34,9 +36,9 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint @@ -113,8 +115,6 @@ interface ApplicationEntryPoint { fun getTransactionSignerFactory(): TransactionSignerFactory - fun getHomeFeatureToggles(): HomeFeatureToggles - fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase fun getOnrampFeatureToggles(): OnrampFeatureToggles @@ -128,4 +128,10 @@ interface ApplicationEntryPoint { fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore + + fun getClipboardManager(): ClipboardManager + + fun getSettingsManager(): SettingsManager + + fun getBlockchainExceptionHandler(): BlockchainExceptionHandler } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt index 296704a5e1..ad37670303 100644 --- a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -8,7 +8,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.message.EventMessageEffect import com.tangem.core.ui.screen.ComposeFragment import com.tangem.utils.Provider import dagger.hilt.android.AndroidEntryPoint @@ -33,11 +32,6 @@ internal class DecomposeFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { component.Content(modifier) - - EventMessageEffect( - messageHandler = uiDependencies.eventMessageHandler, - snackbarHostState = uiDependencies.globalSnackbarHostState, - ) } private class ComponentBuilder>( diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt index 5e2d1c0731..cb7805793c 100644 --- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -2,17 +2,12 @@ package com.tangem.tap import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks -import android.content.Intent import android.os.Bundle -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import java.util.WeakHashMap import kotlin.reflect.KClass -class ForegroundActivityObserver : ActivityResultCaller { - override var activityResultLauncher: ActivityResultLauncher? = null - private set +class ForegroundActivityObserver { private val activities = WeakHashMap, AppCompatActivity>() @@ -26,11 +21,6 @@ class ForegroundActivityObserver : ActivityResultCaller { internal inner class Callbacks : ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { - /* no-op */ - } } override fun onActivityResumed(activity: Activity) { @@ -39,9 +29,6 @@ class ForegroundActivityObserver : ActivityResultCaller { override fun onActivityDestroyed(activity: Activity) { activities.remove(activity::class) - if (activities.isEmpty()) { - activityResultLauncher = null - } } override fun onActivityStarted(activity: Activity) { diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2bc6faa40c..48f1917235 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -7,17 +7,20 @@ import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.MotionEvent -import android.view.View import android.view.WindowManager import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle @@ -36,10 +39,13 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.email.EmailSender -import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase @@ -81,12 +87,12 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel -import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction -import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.tap.routing.toggle.RoutingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.R @@ -209,6 +215,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var dispatchers: CoroutineDispatcherProvider + @Inject + internal lateinit var routingFeatureToggles: RoutingFeatureToggles + + @Inject + internal lateinit var uiDependencies: UiDependencies + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -246,20 +258,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - // window.setHideOverlayWindows(true) - // } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + window.setHideOverlayWindows(true) + } splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } installActivityDependencies() observeAppThemeModeUpdates() - setContentView(R.layout.activity_main) - installRouting() + if (routingFeatureToggles.isNavigationRefactoringEnabled) { + setRootContent() + } else { + setContentView(R.layout.activity_main) + installRouting() + installEventMessageEffect() + } + initContent() - observeStateUpdates() observePolkadotAccountHealthCheck() sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() @@ -271,6 +288,43 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycle.addObserver(WindowObscurationObserver) } + private fun installEventMessageEffect() { + binding.composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + binding.composeView.setContent { + TangemTheme( + activity = this, + uiDependencies = uiDependencies, + overrideSystemBarColors = false, + ) { + EventMessageEffect( + onShowSnackbar = { message, _ -> + showSnackbar( + text = message.message.resolveReference(resources), + length = when (message.duration) { + SnackbarMessage.Duration.Short -> Snackbar.LENGTH_SHORT + SnackbarMessage.Duration.Long -> Snackbar.LENGTH_LONG + SnackbarMessage.Duration.Indefinite -> Snackbar.LENGTH_INDEFINITE + }, + buttonTitle = message.actionLabel?.resolveReference(resources), + action = message.action, + onDismiss = message.onDismissRequest, + ) + }, + ) + } + } + } + + private fun setRootContent() { + val routingComponent = routingComponentFactory.create( + context = rootComponentContext, + ) + + setContent { + routingComponent.Content(Modifier.fillMaxSize()) + } + } + private fun installRouting() { val routingComponent = routingComponentFactory.create( context = rootComponentContext, @@ -297,30 +351,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac is RoutingComponent.Child.LegacyIntent -> { startActivity(child.intent) } + is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child") } } } - private fun observeStateUpdates() { - viewModel.state - .flowWithLifecycle(lifecycle) - .onEach { state -> - if (state.toast is StateEvent.Triggered) { - showToast(state.toast.data) - state.toast.onConsume() - } - } - .launchIn(lifecycleScope) - } - - private fun showToast(toast: Toast) { - dismissSnackbar() - showSnackbar(toast.message, Snackbar.LENGTH_LONG, toast.action.text) { - toast.action.onClick() - dismissSnackbar() - } - } - private fun installActivityDependencies() { cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager @@ -400,8 +435,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop - notificationsHandler = NotificationsHandler(binding.fragmentContainer) + + if (!routingFeatureToggles.isNavigationRefactoringEnabled) { + // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop + notificationsHandler = NotificationsHandler(binding.fragmentContainer) + } navigateToInitialScreenIfNeeded(intent) } @@ -473,7 +511,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac super.onNewIntent(intent) lifecycleScope.launch { - intentProcessor.handleIntent(intent, true) + intentProcessor.handleIntent(intent = intent, isFromForeground = true) } if (intent != null) { @@ -481,22 +519,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - override fun showSnackbar( - @StringRes text: Int, - length: Int, - @StringRes buttonTitle: Int?, - action: View.OnClickListener?, - ) { - showSnackbar(getString(text), length, buttonTitle?.let(::getString), action) + override fun showSnackbar(@StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: (() -> Unit)?) { + showSnackbar( + getString(text), + length, + buttonTitle?.let(::getString), + action = { action?.invoke() }, + ) } - override fun showSnackbar( - text: TextReference, - length: Int, - buttonTitle: TextReference?, - action: View.OnClickListener?, - ) { - showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action) + override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) { + showSnackbar( + text = text.resolveReference(resources), + length = length, + buttonTitle = buttonTitle?.resolveReference(resources), + action = { action?.invoke() }, + ) } override fun dismissSnackbar() { @@ -531,7 +569,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac return if (result) super.dispatchTouchEvent(event) else false } - private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) { + private fun showSnackbar( + text: String, + length: Int, + buttonTitle: String?, + action: (() -> Unit)? = null, + onDismiss: () -> Unit = {}, + ) { if (snackbar != null) return snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply { @@ -542,12 +586,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac setTextColor(textColor) if (buttonTitle != null && action != null) { - setAction(buttonTitle, action) + setAction(buttonTitle, { action() }) } addCallback( object : BaseTransientBottomBar.BaseCallback() { override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + onDismiss() snackbar = null removeCallback(this) } @@ -583,6 +628,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatchNavigationAction { replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) } + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = true, + ) } else { lifecycleScope.launch { val shouldShowTos = !cardRepository.isTangemTOSAccepted() @@ -595,7 +645,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } store.dispatchNavigationAction { replaceAll(route) } - intentProcessor.handleIntent(intentWhichStartedActivity, false) + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = false, + ) } } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 05fd529eaa..3819915526 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -6,6 +6,7 @@ import coil.ImageLoaderFactory import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.Log import com.tangem.TangemSdkLogger +import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.signer.TransactionSignerFactory @@ -20,6 +21,8 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager @@ -50,6 +53,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader @@ -58,7 +62,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.tasks.product.DerivationsFinder -import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -74,7 +77,6 @@ import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository lateinit var store: Store lateinit var foregroundActivityObserver: ForegroundActivityObserver -lateinit var activityResultCaller: ActivityResultCaller internal lateinit var derivationsFinder: DerivationsFinder abstract class TangemApplication : Application(), ImageLoaderFactory { @@ -179,9 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val transactionSignerFactory: TransactionSignerFactory get() = entryPoint.getTransactionSignerFactory() - private val homeFeatureToggles: HomeFeatureToggles - get() = entryPoint.getHomeFeatureToggles() - private val getUserCountryUseCase: GetUserCountryUseCase get() = entryPoint.getGetUserCountryCodeUseCase() @@ -202,6 +201,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val appLogsStore: AppLogsStore get() = entryPoint.getAppLogsStore() + + private val clipboardManager: ClipboardManager + get() = entryPoint.getClipboardManager() + + private val settingsManager: SettingsManager + get() = entryPoint.getSettingsManager() + + private val blockchainExceptionHandler: BlockchainExceptionHandler + get() = entryPoint.getBlockchainExceptionHandler() // endregion override fun onCreate() { @@ -231,7 +239,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { tangemAppLoggerInitializer.initialize() foregroundActivityObserver = ForegroundActivityObserver() - activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) // TODO: Try to performance and user experience. @@ -245,7 +252,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { } loadNativeLibraries() - + ExceptionHandler.append(blockchainExceptionHandler) if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( createNetworkLoggingInterceptor(), @@ -292,7 +299,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - homeFeatureToggles = homeFeatureToggles, getUserCountryUseCase = getUserCountryUseCase, onrampFeatureToggles = onrampFeatureToggles, environmentConfigStorage = environmentConfigStorage, @@ -300,6 +306,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, + clipboardManager = clipboardManager, + settingsManager = settingsManager, ), ), ) @@ -355,6 +363,5 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { ) factory.build(Analytics, buildData) - // ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA] } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt index 53228bf674..2d8f270f89 100644 --- a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common -import android.view.View import androidx.annotation.StringRes import com.google.android.material.snackbar.Snackbar import com.tangem.core.ui.extensions.TextReference @@ -11,14 +10,14 @@ interface SnackbarHandler { @StringRes text: Int, length: Int = Snackbar.LENGTH_INDEFINITE, @StringRes buttonTitle: Int? = null, - action: View.OnClickListener? = null, + action: (() -> Unit)? = null, ) fun showSnackbar( text: TextReference, length: Int = Snackbar.LENGTH_INDEFINITE, buttonTitle: TextReference? = null, - action: View.OnClickListener? = null, + action: (() -> Unit)? = null, ) fun dismissSnackbar() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index f35a636601..e1dfdea4c1 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -13,6 +13,7 @@ sealed class Onboarding( class Started : Onboarding("Onboarding", "Onboarding Started") class Finished : Onboarding("Onboarding", "Onboarding Finished") + data object OfflineAttestationFailed : Onboarding(category = "Onboarding", event = "Offline Attestation Failed") sealed class CreateWallet( event: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt new file mode 100644 index 0000000000..559fc3418b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsEventConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +internal class FirebaseAnalyticsEventConverter { + + fun convertEventName(event: String): String { + return convertString(event, FIREBASE_EVENT_NAME_MAX_LENGTH) + } + + fun convertEventParams(params: Map): Map { + return params.map { (key, value) -> + convertString(key, FIREBASE_EVENT_NAME_MAX_LENGTH) to convertString(value, FIREBASE_EVENT_VALUE_MAX_LENGTH) + }.toMap() + } + + private fun convertString(string: String, maxLength: Int): String { + return string + .replace(REPLACING_PATTERN.toRegex(), WORD_SEPARATOR) + .trim { it in TRIMMING_CHARACTERS } + .trimToLength(maxLength) + } + + private fun String.trimToLength(length: Int): String { + return if (this.length > length) this.substring(0, length) else this + } + + private companion object { + const val REPLACING_PATTERN = "[^\\w]+" + const val WORD_SEPARATOR = "_" + const val TRIMMING_CHARACTERS = WORD_SEPARATOR + const val FIREBASE_EVENT_NAME_MAX_LENGTH = 40 + const val FIREBASE_EVENT_VALUE_MAX_LENGTH = 100 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 20597e0fb9..7e4e5085bc 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -18,12 +18,19 @@ internal class FirebaseClient : FirebaseAnalyticsClient { private val fbAnalytics = Firebase.analytics private val fbCrashlytics = Firebase.crashlytics + private val eventConverter = FirebaseAnalyticsEventConverter() + override fun logEvent(event: String, params: Map) { - fbAnalytics.logEvent(event, params.toBundle()) + fbAnalytics.logEvent( + eventConverter.convertEventName(event), + eventConverter.convertEventParams(params).toBundle(), + ) } override fun logErrorEvent(error: Throwable, params: Map) { - params.forEach { fbCrashlytics.setCustomKey(it.key, it.value) } + eventConverter.convertEventParams(params) + .forEach { fbCrashlytics.setCustomKey(it.key, it.value) } + fbCrashlytics.recordException(error) } diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt index 8b7ee07933..73f2ebd5ef 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -1,12 +1,52 @@ package com.tangem.tap.common.clipboard import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN +import android.os.Build +import android.os.PersistableBundle import com.tangem.core.ui.clipboard.ClipboardManager +import timber.log.Timber import android.content.ClipboardManager as AndroidClipboardManager internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager { - override fun setText(label: String, text: String) { - clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) + override fun setText(text: String, isSensitive: Boolean, label: String) { + val clip = ClipData.newPlainText(label, text).apply { + if (isSensitive) description.setAsSensitive() + } + + clipboardManager.setPrimaryClip(clip) + } + + override fun getText(default: String?): String? { + val clip = clipboardManager.primaryClip + + if (clip == null || clip.itemCount == 0) { + Timber.d("Clipboard is empty") + return default + } + + val clipDescription = clipboardManager.primaryClipDescription + if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) { + Timber.d("Clipboard doesn't contain text") + return default + } + + return clip.getItemAt(0).text?.toString() + } + + private fun ClipDescription.setAsSensitive() { + extras = PersistableBundle().apply { + putBoolean(getExtraIsSensitiveFlag(), true) + } + } + + private fun getExtraIsSensitiveFlag(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ClipDescription.EXTRA_IS_SENSITIVE + } else { + "android.content.extra.IS_SENSITIVE" + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt index bdab75088f..8013aa18f1 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -4,8 +4,10 @@ import com.tangem.core.ui.clipboard.ClipboardManager import timber.log.Timber internal object MockClipboardManager : ClipboardManager { - override fun setText(label: String, text: String) { - /** Intentionnaly do nothing */ + + override fun setText(text: String, isSensitive: Boolean, label: String) { Timber.w("Clipboard Manager not available") } + + override fun getText(default: String?): String? = null } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt deleted file mode 100644 index ec4f3737f0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.tap.common.extensions - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import androidx.core.app.ShareCompat -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import java.io.File - -/** - * required since targetAndroid=30 - * - * - * - * - * - * - */ -@Deprecated("Use EmailSender instead") -fun Activity.sendEmail( - email: String, - subject: String, - message: String, - file: File? = null, - onFail: ((Exception) -> Unit)? = null, -) { - fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent { - val builder = ShareCompat.IntentBuilder.from(this) - .setType("message/rfc822") - .setEmailTo(arrayOf(recipient)) - .setSubject(subject) - .setText(text) - file?.let { builder.setStream(FileProvider.getUriForFile(this, "$packageName.provider", it)) } - return builder.intent - } - - val originalIntent = createEmailShareIntent(email, subject, message, file) - val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) - - val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0) - val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0) - - val targetedIntents = originalIntentResults - .filter { originalResult -> - emailFilterIntentResults.any { - originalResult.activityInfo.packageName == it.activityInfo.packageName - } - } - .map { - createEmailShareIntent(email, subject, message, file).apply { - setPackage(it.activityInfo.packageName) - } - } - .toMutableList() - try { - val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...") - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ContextCompat.startActivity(this, chooserIntent, null) - } catch (ex: Exception) { - onFail?.invoke(ex) - } -} \ No newline at end of file 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 deleted file mode 100644 index b6101bd4d8..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.tap.common.extensions - -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 b14e9e4090..355da51669 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 @@ -2,26 +2,19 @@ package com.tangem.tap.common.extensions -import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager import android.content.Context -import android.content.ContextWrapper import android.graphics.drawable.Drawable import android.view.View -import androidx.annotation.* +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? { return ContextCompat.getDrawable(this, drawableResId) } -@ColorInt -fun Fragment.getColor(@ColorRes colorRes: Int): Int { - return ContextCompat.getColor(requireContext(), colorRes) -} - @ColorInt fun Context.getColorCompat(@ColorRes colorRes: Int): Int { return ContextCompat.getColor(this, colorRes) @@ -40,10 +33,6 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String { return context.getString(id, *formatArgs) } -fun View.getQuantityString(@PluralsRes id: Int, quantity: Int): String { - return context.resources.getQuantityString(id, quantity, quantity) -} - fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) { return if (show) this.show(invokeBeforeStateChanged) else this.hide(invokeBeforeStateChanged) } @@ -62,25 +51,6 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) { this.visibility = View.GONE } -tailrec fun Context?.getActivity(): Activity? = this as? Activity - ?: (this as? ContextWrapper)?.baseContext?.getActivity() - -fun Context.copyToClipboard(value: Any, label: String = "") { - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return - - val clip: ClipData = ClipData.newPlainText(label, value.toString()) - clipboard.setPrimaryClip(clip) -} - -fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? { - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - ?: return default - val clipData = clipboard.primaryClip ?: return default - if (clipData.itemCount == 0) return default - - return clipData.getItemAt(0).text -} - fun View.getString(resId: Int, vararg formatArgs: Any?): String { return context.getString(resId, *formatArgs) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 4fed318b36..04d6fa4896 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -1,8 +1,5 @@ package com.tangem.tap.common.redux -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.global.NetworkServices import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.legacy.LegacyMiddleware @@ -28,7 +25,6 @@ import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import org.rekotlin.Middleware import org.rekotlin.StateType @@ -46,18 +42,6 @@ data class AppState( val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { - private val domainState: DomainState - get() = domainStore.state - - private val domainNetworks: NetworkServices - get() = domainState.globalState.networkServices - - val featureRepositoryProvider: FeatureRepositoryProvider - get() = FeatureRepositoryProvider( - tangemTechApi = domainNetworks.tangemTechService.api, - dispatchers = AppCoroutineDispatcherProvider(), - ) - companion object { fun getMiddleware(): List> { return listOf( diff --git a/app/src/main/java/com/tangem/tap/common/redux/FeatureRepositoryProvider.kt b/app/src/main/java/com/tangem/tap/common/redux/FeatureRepositoryProvider.kt deleted file mode 100644 index 55e4c0b653..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/FeatureRepositoryProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.common.redux - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.tap.features.home.data.HomeRepositoryImpl -import com.tangem.tap.features.home.domain.HomeRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider - -class FeatureRepositoryProvider(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider) { - - val homeRepository: HomeRepository = HomeRepositoryImpl(tangemTechApi, dispatchers) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 740f5bd460..87fae53bc3 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 @@ -82,8 +82,4 @@ sealed class GlobalAction : Action { object Update : GlobalAction() } - - object FetchUserCountry : GlobalAction() { - data class Success(val countryCode: String) : GlobalAction() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index b8865305c5..e7f1d6613d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware -import java.util.Locale object GlobalMiddleware { val handler = globalMiddlewareHandler @@ -87,27 +86,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } scope.launch { exchangeManager.update() } } - is GlobalAction.FetchUserCountry -> { - val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles) - - if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) { - scope.launch { - runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() } - .onSuccess { - store.dispatchOnMain( - GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()), - ) - } - .onFailure { - store.dispatchOnMain( - GlobalAction.FetchUserCountry.Success( - countryCode = Locale.getDefault().country.lowercase(), - ), - ) - } - } - } - } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index d55f99d21b..c05d9bbc89 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -1,7 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.proxy.AppStateHolder @@ -38,7 +36,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde } is GlobalAction.SaveScanResponse -> { appStateHolder.scanResponse = action.scanResponse - domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse)) globalState.copy(scanResponse = action.scanResponse) } is GlobalAction.ChangeAppCurrency -> { @@ -77,9 +74,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde } is GlobalAction.SetIfCardVerifiedOnline -> globalState.copy(cardVerifiedOnline = action.verified) - is GlobalAction.FetchUserCountry.Success -> { - globalState.copy(userCountryCode = action.countryCode) - } else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt index 145073e75c..ab6dd43f18 100644 --- a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt @@ -3,18 +3,46 @@ package com.tangem.tap.common.settings import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.hardware.biometrics.BiometricManager import android.net.Uri +import android.os.Build import android.provider.Settings import com.tangem.core.navigation.settings.SettingsManager internal class IntentSettingsManager(val context: Context) : SettingsManager { - override fun openSettings() { - val openSettingsIntent = Intent( + + override fun openAppSettings() { + val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null), - ).apply { - flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + ) + + open(intent = intent) + } + + override fun openBiometricSettings() { + val settingsAction = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Settings.ACTION_BIOMETRIC_ENROLL + else -> Settings.ACTION_SECURITY_SETTINGS } - context.startActivity(openSettingsIntent) + + val intent = Intent(settingsAction).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG, + ) + } + } + + open(intent = intent) + } + + private fun open(intent: Intent) { + context.startActivity( + intent.apply { + flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + }, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt index 8b2cb093dd..18882c44dd 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.ui import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.widget.Toast import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics @@ -10,6 +11,7 @@ import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.model.WalletAddressData +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding @@ -51,7 +53,12 @@ internal class AddressInfoBottomSheetDialog( tvAddress.text = data.address btnFlCopyAddress.setOnClickListener { Analytics.send(Token.Receive.ButtonCopyAddress()) - context.copyToClipboard(data.address) + + Toast + .makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT) + .show() + + store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true) } btnFlShare.setOnClickListener { Analytics.send(Token.Receive.ButtonShareAddress()) diff --git a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt index e38ae3919c..8d7beb3948 100644 --- a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt +++ b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt @@ -68,5 +68,6 @@ internal class AndroidEmailSender : EmailSender { } return builder.intent + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt new file mode 100644 index 0000000000..7e975e8f02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -0,0 +1,54 @@ +package com.tangem.tap.data + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.services.secure.SecureStorage +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultVisaAuthTokenStorage @Inject constructor( + @ApplicationContext applicationContext: Context, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : VisaAuthTokenStorage { + + private val secureStorage = AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "visa_auth_storage", + ), + ) + + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java) + + override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { + val json = tokensAdapter.toJson(tokens) + + secureStorage.store( + json.encodeToByteArray(throwOnInvalidSequence = true), + VISA_AUTH_TOKENS_KEY, + ) + } + + override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) { + secureStorage.get(VISA_AUTH_TOKENS_KEY) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) + } + + private companion object { + const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt new file mode 100644 index 0000000000..85c60d494d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.data + +import android.content.Context +import com.tangem.common.services.secure.SecureStorage +import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultVisaOTPStorage @Inject constructor( + @ApplicationContext applicationContext: Context, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : VisaOTPStorage { + + private val secureStorage = AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "visa_otp_storage", + ), + ) + + override suspend fun saveOTP(cardId: String, otp: ByteArray) = withContext(dispatcherProvider.io) { + secureStorage.store(otp, VISA_OTP_KEY_PREFIX + cardId) + } + + override suspend fun getOTP(cardId: String): ByteArray? = withContext(dispatcherProvider.io) { + secureStorage.get(VISA_OTP_KEY_PREFIX + cardId) + } + + override suspend fun removeOTP(cardId: String) = withContext(dispatcherProvider.io) { + secureStorage.delete(VISA_OTP_KEY_PREFIX + cardId) + } + + private companion object { + const val VISA_OTP_KEY_PREFIX = "visa_otp_" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt index 1128b32708..b59094d579 100644 --- a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt +++ b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt @@ -6,8 +6,9 @@ import com.arkivanov.decompose.defaultComponentContext import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.ui.UiDependencies +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -27,13 +28,13 @@ internal object RootAppComponentContextModule { @ActivityContext context: Context, dispatchers: CoroutineDispatcherProvider, componentBuilder: DecomposeComponent.Builder, - uiDependencies: UiDependencies, + @GlobalUiMessageSender messageSender: UiMessageSender, ): AppComponentContext { return DefaultAppComponentContext( componentContext = (context as AppCompatActivity).defaultComponentContext(), - messageHandler = uiDependencies.eventMessageHandler, dispatchers = dispatchers, hiltComponentBuilder = componentBuilder, + messageSender = messageSender, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 4754c5ec8e..d741fc1a41 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -3,9 +3,12 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.BuildConfig import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask +import com.tangem.tap.domain.visa.VisaCardScanHandler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,18 +18,27 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class TangemSdkManagerModule { +internal class TangemSdkManagerModule { @Provides @Singleton fun provideTangemSdkManager( @ApplicationContext context: Context, cardSdkConfigRepository: CardSdkConfigRepository, + visaCardScanHandler: VisaCardScanHandler, + visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { MockTangemSdkManager(resources = context.resources) } else { - DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) + DefaultTangemSdkManager( + cardSdkConfigRepository = cardSdkConfigRepository, + resources = context.resources, + visaCardScanHandler = visaCardScanHandler, + visaCardActivationTaskFactory = visaCardActivationTaskFactory, + onboardingV2FeatureToggles = onboardingV2FeatureToggles, + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt index 44631d4c05..de5f7ef9e3 100644 --- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -1,6 +1,9 @@ package com.tangem.tap.di import androidx.compose.material3.SnackbarHostState +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.DefaultUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.EventMessageHandler @@ -28,4 +31,11 @@ internal object UiDependenciesModule { override val eventMessageHandler: EventMessageHandler = EventMessageHandler() } } + + @Provides + @Singleton + @GlobalUiMessageSender + fun provideUiMessageSender(uiDependencies: UiDependencies): UiMessageSender { + return DefaultUiMessageSender(uiDependencies.eventMessageHandler) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt new file mode 100644 index 0000000000..c78909a676 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.data + +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.tap.data.DefaultVisaAuthTokenStorage +import com.tangem.tap.data.DefaultVisaOTPStorage +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface VisaStorageModule { + + @Binds + @Singleton + fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage + + @Binds + @Singleton + fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 38adc6ec9e..7be9d95b9a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -6,6 +6,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor +import com.tangem.tap.domain.scanCard.LegacyScanProcessor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,7 +25,9 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor() + fun provideScanCardProcessor(legacyScanProcessor: LegacyScanProcessor): ScanCardProcessor { + return DefaultScanCardProcessor(legacyScanProcessor = legacyScanProcessor) + } @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt new file mode 100644 index 0000000000..381de50f84 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.promo.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object PromoDomainModule { + + @Provides + @Singleton + fun provideShouldShowSwapPromoWalletUseCase( + promoSettingsRepository: PromoRepository, + ): ShouldShowSwapPromoWalletUseCase { + return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository) + } + + @Provides + @Singleton + fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase { + return ShouldShowRingPromoUseCase(promoRepository) + } + + @Provides + @Singleton + fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase { + return ShouldShowSwapPromoTokenUseCase(promoRepository) + } + + @Provides + @Singleton + fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase { + return ShouldShowSwapStoriesUseCase(promoRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 352171f53a..6871b3101c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -8,7 +8,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PermissionRepository -import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase @@ -117,30 +116,6 @@ internal object SettingsDomainModule { return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) } - @Provides - @Singleton - fun provideShouldShowSwapPromoWalletUseCase( - promoSettingsRepository: PromoSettingsRepository, - ): ShouldShowSwapPromoWalletUseCase { - return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository) - } - - @Provides - @Singleton - fun provideShouldShowRingPromoUseCase( - promoSettingsRepository: PromoSettingsRepository, - ): ShouldShowRingPromoUseCase { - return ShouldShowRingPromoUseCase(promoSettingsRepository) - } - - @Provides - @Singleton - fun provideShouldShowSwapPromoTokenUseCase( - promoSettingsRepository: PromoSettingsRepository, - ): ShouldShowSwapPromoTokenUseCase { - return ShouldShowSwapPromoTokenUseCase(promoSettingsRepository) - } - @Provides @Singleton fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 9bea3ac624..1014dca16a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -1,12 +1,10 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -118,13 +116,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - swapRepository: SwapRepository, currencyChecksRepository: CurrencyChecksRepository, - showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, - promoRepository: PromoRepository, stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, - rampStateManager: RampStateManager, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, @@ -132,12 +126,8 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, networksRepository = networksRepository, currencyChecksRepository = currencyChecksRepository, - swapRepository = swapRepository, - showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, - promoRepository = promoRepository, stakingRepository = stakingRepository, dispatchers = dispatchers, - rampStateManager = rampStateManager, ) } diff --git a/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt index aca0f1678a..628d889d40 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di.routing -import com.tangem.tap.routing.RoutingComponent -import com.tangem.tap.routing.impl.DefaultRoutingComponent +import com.tangem.tap.routing.component.RoutingComponent +import com.tangem.tap.routing.component.impl.DefaultRoutingComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index a19ec7e918..a7592add78 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -10,7 +10,9 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store // TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead -internal class DefaultScanCardProcessor : ScanCardProcessor { +internal class DefaultScanCardProcessor( + private val legacyScanProcessor: LegacyScanProcessor, +) : ScanCardProcessor { private val isNewCardScanningEnabled: Boolean get() = store.inject(DaggerGraphState::cardScanningFeatureToggles).isNewCardScanningEnabled @@ -21,7 +23,10 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { return if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) } else { - LegacyScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) + legacyScanProcessor.scan( + cardId = cardId, + allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + ) } } @@ -38,24 +43,24 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { ) { if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan( - analyticsSource, - cardId, - onProgressStateChange, - onWalletNotCreated, - disclaimerWillShow, - onFailure, - onSuccess, + analyticsSource = analyticsSource, + cardId = cardId, + onProgressStateChange = onProgressStateChange, + onWalletNotCreated = onWalletNotCreated, + disclaimerWillShow = disclaimerWillShow, + onFailure = onFailure, + onSuccess = onSuccess, ) } else { - LegacyScanProcessor.scan( - analyticsSource, - cardId, - onProgressStateChange, - onWalletNotCreated, - disclaimerWillShow, - onCancel, - onFailure, - onSuccess, + legacyScanProcessor.scan( + analyticsSource = analyticsSource, + cardId = cardId, + onProgressStateChange = onProgressStateChange, + onWalletNotCreated = onWalletNotCreated, + disclaimerWillShow = disclaimerWillShow, + onCancel = onCancel, + onFailure = onFailure, + onSuccess = onSuccess, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index b1810f1745..96137b75ea 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -2,13 +2,22 @@ package com.tangem.tap.domain.scanCard import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull @@ -18,6 +27,8 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.sdk.extensions.localizedDescriptionRes +import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog @@ -37,8 +48,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton -internal object LegacyScanProcessor { +@Singleton +internal class LegacyScanProcessor @Inject constructor( + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, +) { suspend fun scan( cardId: String? = null, @@ -48,6 +65,9 @@ internal object LegacyScanProcessor { cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, ) + .doOnFailure { error -> + onScanFailure(error = error, onFailure = {}, onCancel = {}) + } } @Suppress("LongParameterList") @@ -72,7 +92,16 @@ internal object LegacyScanProcessor { result .doOnFailure { error -> - onFailure(error) + onScanFailure( + error = error, + onFailure = onFailure, + onCancel = { + mainScope.launch { + onProgressStateChange.invoke(false) + onCancel() + } + }, + ) } .doOnSuccess { scanResponse -> tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) @@ -82,7 +111,6 @@ internal object LegacyScanProcessor { showDisclaimerIfNeed( scanResponse = scanResponse, disclaimerWillShow = disclaimerWillShow, - onFailure = onFailure, nextHandler = { scanResponse2 -> onScanSuccess( scanResponse = scanResponse2, @@ -114,7 +142,6 @@ internal object LegacyScanProcessor { scanResponse: ScanResponse, crossinline disclaimerWillShow: () -> Unit = {}, crossinline nextHandler: suspend (ScanResponse) -> Unit, - crossinline onFailure: suspend (error: TangemError) -> Unit, ) { val disclaimer = scanResponse.card.createDisclaimer() @@ -135,6 +162,49 @@ internal object LegacyScanProcessor { } } + private suspend inline fun onScanFailure( + error: TangemError, + crossinline onFailure: suspend (TangemError) -> Unit, + crossinline onCancel: () -> Unit, + ) { + if (error is TangemSdkError.CardVerificationFailed) { + analyticsEventHandler.send(event = Onboarding.OfflineAttestationFailed) + + val resource = error.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(id = resId, resArgs.toWrappedList()), + title = resourceReference(id = R.string.security_alert_title), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(id = R.string.alert_button_request_support), + onClick = { + mainScope.launch { + onCancel() + + store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke( + type = FeedbackEmailType.CardAttestationFailed, + ) + } + }, + ) + }, + secondActionBuilder = { + cancelAction { + mainScope.launch { onCancel() } + } + }, + ), + ) + } else { + onFailure(error) + } + } + @Suppress("LongMethod", "MagicNumber") private suspend inline fun onScanSuccess( scanResponse: ScanResponse, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 59dea35070..7ce6aa9702 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -15,6 +15,7 @@ import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic +import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -23,7 +24,11 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaCardActivationResponse import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask @@ -35,23 +40,28 @@ import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.* +import com.tangem.tap.domain.tasks.product.CreateProductWalletTask +import com.tangem.tap.domain.tasks.product.ResetBackupCardTask +import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask +import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask +import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.wallet.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions", "LargeClass") -class DefaultTangemSdkManager( +internal class DefaultTangemSdkManager( private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources, + private val visaCardScanHandler: VisaCardScanHandler, + private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : TangemSdkManager { private val awaitInitializationMutex = Mutex() @@ -122,16 +132,21 @@ class DefaultTangemSdkManager( messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { - val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) - return runTaskAsyncReturnOnMain( - runnable = ScanProductTask( - card = null, - derivationsFinder = derivationsFinder, - allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, - ), - cardId = cardId, - initialMessage = message, - ).also { sendScanResultsToAnalytics(it) } + val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = ScanProductTask( + card = null, + derivationsFinder = derivationsFinder, + allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + visaCardScanHandler = visaCardScanHandler, + visaCoroutineScope = this, + onboardingV2FeatureToggles = onboardingV2FeatureToggles, + ), + cardId = cardId, + initialMessage = message, + ).also { sendScanResultsToAnalytics(it) } + } } override suspend fun createProductWallet( @@ -154,9 +169,9 @@ class DefaultTangemSdkManager( ), cardId = scanResponse.card.cardId, initialMessage = if (scanResponse.cardTypesResolver.isRing()) { - Message(resources.getString(R.string.initial_message_create_wallet_body_ring)) + Message(resources.getStringSafe(R.string.initial_message_create_wallet_body_ring)) } else { - Message(resources.getString(R.string.initial_message_create_wallet_body)) + Message(resources.getStringSafe(R.string.initial_message_create_wallet_body)) }, iconScanRes = if (scanResponse.cardTypesResolver.isRing()) R.drawable.img_hand_scan_ring else null, preflightReadFilter = null, @@ -194,9 +209,9 @@ class DefaultTangemSdkManager( ), cardId = scanResponse.card.cardId, initialMessage = if (scanResponse.cardTypesResolver.isRing()) { - Message(resources.getString(R.string.initial_message_create_wallet_body_ring)) + Message(resources.getStringSafe(R.string.initial_message_create_wallet_body_ring)) } else { - Message(resources.getString(R.string.initial_message_create_wallet_body)) + Message(resources.getStringSafe(R.string.initial_message_create_wallet_body)) }, preflightReadFilter = null, ) @@ -243,7 +258,7 @@ class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, ), cardId = cardId, - initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), + initialMessage = Message(resources.getStringSafe(R.string.card_settings_reset_card_to_factory)), ) } @@ -251,7 +266,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( runnable = ResetBackupCardTask(userWalletId), initialMessage = Message( - resources.getString( + resources.getStringSafe( R.string.initial_message_reset_backup_card_header, cardNumber.toString(), ), @@ -281,7 +296,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, - initialMessage = Message(resources.getString(R.string.initial_message_change_passcode_body)), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_change_passcode_body)), ) } @@ -289,7 +304,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, - initialMessage = Message(resources.getString(R.string.initial_message_change_access_code_body)), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_change_access_code_body)), ) } @@ -297,7 +312,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, - initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) } @@ -308,7 +323,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( SetUserCodeRecoveryAllowedTask(enabled), cardId, - initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) } @@ -319,7 +334,7 @@ class DefaultTangemSdkManager( return runTaskAsyncReturnOnMain( runnable = ScanTask(allowRequestAccessCodeFromRepository), cardId = cardId, - initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) .map { CardDTO(it) } } @@ -372,7 +387,9 @@ class DefaultTangemSdkManager( @Deprecated("TangemSdkManager shouldn't returns a string from resources") override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { - return resources.getString(stringResId, *formatArgs) + val args = formatArgs.toSet().filterNotNull().toTypedArray() + + return resources.getStringSafe(stringResId, *args) } override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { @@ -457,6 +474,28 @@ class DefaultTangemSdkManager( // endregion + // region Visa-specific + + override suspend fun activateVisaCard( + accessCode: String, + challengeToSign: VisaAuthChallenge.Card?, + activationInput: VisaActivationInput, + ): CompletionResult { + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = visaCardActivationTaskFactory.create( + accessCode = accessCode, + challengeToSign = challengeToSign, + activationInput = activationInput, + coroutineScope = this, + ), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + } + + // endregion + companion object { private const val MAX_INITIALIZE_ATTEMPTS = 10 } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 471a9436e2..c0dd2c2c03 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -12,10 +12,14 @@ import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.InMemoryStorage +import com.tangem.core.res.getStringSafe import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaCardActivationResponse import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter @@ -150,7 +154,9 @@ class MockTangemSdkManager( @Deprecated("TangemSdkManager shouldn't returns a string from resources") override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { - return resources.getString(stringResId, *formatArgs) + val args = formatArgs.toSet().filterNotNull().toTypedArray() + + return resources.getStringSafe(stringResId, *args) } override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { @@ -191,4 +197,14 @@ class MockTangemSdkManager( } // endregion + + // region Visa-specific + + override suspend fun activateVisaCard( + accessCode: String, + challengeToSign: VisaAuthChallenge.Card?, + activationInput: VisaActivationInput, + ): CompletionResult { + error("Not implemented") + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 2fcfc2411a..bc2268c5a9 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 @@ -9,6 +9,7 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.deserialization.WalletDataDeserializer import com.tangem.common.extensions.* +import com.tangem.common.map import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils @@ -22,11 +23,13 @@ import com.tangem.domain.common.TapWorkarounds.isVisa import com.tangem.domain.common.TwinsHelper import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -35,16 +38,21 @@ import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.TapSdkError +import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.collections.set internal class ScanProductTask( private val card: Card?, private val derivationsFinder: DerivationsFinder?, + private val visaCardScanHandler: VisaCardScanHandler?, + private val visaCoroutineScope: CoroutineScope?, + private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -61,6 +69,23 @@ internal class ScanProductTask( return } + if (VisaUtilities.isVisaCard(cardDto)) { + if (onboardingV2FeatureToggles == null || + onboardingV2FeatureToggles.isVisaOnboardingEnabled.not() + ) { + callback(CompletionResult.Failure(TangemSdkError.NotSupportedFirmwareVersion())) + return + } + + readVisaCard( + session = session, + cardDto = cardDto, + scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + callback = callback, + ) + return + } + val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() else -> ScanWalletProcessor(derivationsFinder) @@ -96,6 +121,43 @@ internal class ScanProductTask( } return null } + + private fun readVisaCard( + session: CardSession, + cardDto: CardDTO, + scanWalletProcessor: ScanWalletProcessor, + callback: (result: CompletionResult) -> Unit, + ) { + visaCardScanHandler ?: run { + callback(CompletionResult.Failure(TangemSdkError.InsNotSupported())) + return + } + + visaCoroutineScope ?: run { + callback(CompletionResult.Failure(TangemSdkError.InsNotSupported())) + return + } + + visaCoroutineScope.launch { + when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) { + is CompletionResult.Success -> { + scanWalletProcessor.proceed( + card = cardDto, + session = session, + ) { scanResponseResult -> + callback( + scanResponseResult.map { scanResponse -> + scanResponse.copy(visaCardActivationStatus = result.data) + }, + ) + } + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } } private class ScanWalletProcessor( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt new file mode 100644 index 0000000000..184e2a3f63 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -0,0 +1,268 @@ +package com.tangem.tap.domain.tasks.visa + +import com.reown.util.hexToBytes +import com.tangem.common.CompletionResult +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.toHexString +import com.tangem.common.map +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.operations.GenerateOTPCommand +import com.tangem.operations.attestation.AttestCardKeyCommand +import com.tangem.operations.pins.SetUserCodeCommand +import com.tangem.operations.sign.SignHashCommand +import com.tangem.operations.sign.SignHashResponse +import com.tangem.operations.wallet.CreateWalletTask +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.* +import kotlin.coroutines.resume +import kotlin.jvm.Throws + +@Suppress("LongParameterList") +class VisaCardActivationTask @AssistedInject constructor( + @Assisted private val accessCode: String, + @Assisted private val challengeToSign: VisaAuthChallenge.Card?, + @Assisted private val activationInput: VisaActivationInput, + @Assisted private val coroutineScope: CoroutineScope, + private val otpStorage: VisaOTPStorage, + private val visaAuthTokenStorage: VisaAuthTokenStorage, + private val visaAuthRepository: VisaAuthRepository, + private val visaActivationRepository: VisaActivationRepository, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + coroutineScope.launch { + callback(runSuspend(session)) + } + } + + private suspend fun runSuspend(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + if (card.cardId != activationInput.cardId) { + return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message)) + } + + return if (challengeToSign != null) { + signAuthorizationChallenge(session, challengeToSign) + } else { + val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() } + .getOrElse { + return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) + } + + signOrder(session, activationOrder) + } + } + + private suspend fun signAuthorizationChallenge( + session: CardSession, + challengeToSign: VisaAuthChallenge.Card, + ): CompletionResult { + val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes()) + val result = suspendCancellableCoroutine { continuation -> + attestationCommand.run(session = session) { attestationResponse -> + continuation.resume(attestationResponse) + } + } + + return when (result) { + is CompletionResult.Success -> { + processSignedAuthorizationChallenge( + session = session, + signedChallenge = challengeToSign.toSignedChallenge( + signedChallenge = result.data.cardSignature.toHexString(), + salt = result.data.salt.toHexString(), + ), + ) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + + private suspend fun processSignedAuthorizationChallenge( + session: CardSession, + signedChallenge: VisaAuthSignedChallenge, + ): CompletionResult { + return coroutineScope { + val activationOrderDeferred = async { getActivationOrderToSign(signedChallenge) } + val otpTaskDeferred = async { createWallet(session) } + + val order = runCatching { activationOrderDeferred.await() } + .getOrElse { + otpTaskDeferred.cancel() + return@coroutineScope CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) + } + + otpTaskDeferred.await() + + signOrder(session, order) + } + } + + @Throws(TangemSdkError::class) + private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder { + val tokens = runCatching { + visaAuthRepository.getAccessTokens(signedChallenge) + }.getOrElse { + throw TangemSdkError.Underlying( + "Underlying network error: ${it.message ?: ""}", + ) + } + + visaAuthTokenStorage.store(tokens) + + return visaActivationRepository.getActivationOrderToSign() + } + + private suspend fun createWallet(session: CardSession): CompletionResult { + coroutineScope { ensureActive() } + + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + return if (card.wallets.any { it.curve == VisaUtilities.mandatoryCurve }) { + createOTP(session) + } else { + val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve) + val result = suspendCancellableCoroutine { continuation -> + createWalletTask.run(session) { createWalletResult -> + continuation.resume(createWalletResult) + } + } + + when (result) { + is CompletionResult.Success -> { + createOTP(session) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + } + + private suspend fun createOTP(session: CardSession): CompletionResult { + coroutineScope { ensureActive() } + + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val otp = otpStorage.getOTP(card.cardId) + return if (otp != null) { + CompletionResult.Success(Unit) + } else { + val otpCommand = GenerateOTPCommand() + val result = suspendCancellableCoroutine { continuation -> + otpCommand.run(session) { otpResult -> + continuation.resume(otpResult) + } + } + + when (result) { + is CompletionResult.Success -> { + otpStorage.saveOTP(card.cardId, result.data.rootOTP) + CompletionResult.Success(Unit) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + } + + private suspend fun signOrder( + session: CardSession, + order: ActivationOrder, + ): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = + card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey) + val result = suspendCancellableCoroutine { continuation -> + task.run(session) { signResult -> + continuation.resume(signResult) + } + } + + return when (result) { + is CompletionResult.Success -> { + handleSignedOrder( + session = session, + activationOrder = order, + response = result.data, + ) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + + private suspend fun handleSignedOrder( + session: CardSession, + activationOrder: ActivationOrder, + response: SignHashResponse, + ): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val signedOrder = SignedActivationOrder( + activationOrder = activationOrder, + signature = response.signature.toHexString(), + ) + + val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure( + TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message), + ) + + val activationResponse = VisaCardActivationResponse( + signedActivationOrder = signedOrder, + rootOTP = VisaRootOTP(otp.toHexString()), + ) + + return setupAccessCode(session).map { activationResponse } + } + + private suspend fun setupAccessCode(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + if (card.isAccessCodeSet) { + return CompletionResult.Success(Unit) + } + + val task = SetUserCodeCommand.changeAccessCode(accessCode) + val result = suspendCancellableCoroutine { continuation -> + task.run(session) { setAccessCodeResult -> + continuation.resume(setAccessCodeResult) + } + } + + return when (result) { + is CompletionResult.Success -> { + CompletionResult.Success(Unit) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + + @AssistedFactory + interface Factory { + fun create( + accessCode: String, + challengeToSign: VisaAuthChallenge.Card?, + activationInput: VisaActivationInput, + coroutineScope: CoroutineScope, + ): VisaCardActivationTask + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 4e3364feba..8d2c83906c 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -23,8 +23,13 @@ class FinalizeTwinTask( PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> - ScanProductTask(readResult.data, derivationsFinder = null) - .run(session, callback) + ScanProductTask( + readResult.data, + derivationsFinder = null, + visaCardScanHandler = null, + visaCoroutineScope = null, + onboardingV2FeatureToggles = null, + ).run(session, callback) is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 2885696de1..c0d95f3122 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.models.scan.serialization.* +import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences @@ -60,6 +61,7 @@ internal object UserWalletsListManagerModule { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaCardActivationStatus.jsonAdapter) .add(KotlinJsonAdapterFactory()) .build() diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt new file mode 100644 index 0000000000..c231b0ba49 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -0,0 +1,285 @@ +package com.tangem.tap.domain.visa + +import com.tangem.common.CompletionResult +import com.tangem.common.card.CardWallet +import com.tangem.common.core.CardSession +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.operations.attestation.AttestCardKeyCommand +import com.tangem.operations.attestation.AttestCardKeyResponse +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.sign.SignHashCommand +import com.tangem.operations.sign.SignHashResponse +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.resume + +internal class VisaCardScanHandler @Inject constructor( + private val visaAuthRepository: VisaAuthRepository, + private val visaActivationRepository: VisaActivationRepository, + private val visaAuthTokenStorage: VisaAuthTokenStorage, +) { + + suspend fun handleVisaCardScan(session: CardSession): CompletionResult { + Timber.i("Attempting to handle Visa card scan") + + val card = session.environment.card ?: run { + Timber.e("Card is null") + return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + } + + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { + val activationInput = + VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet) + val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) + return CompletionResult.Success(activationStatus) + } + + return deriveKey(wallet, session) + } + + private suspend fun deriveKey( + wallet: CardWallet, + session: CardSession, + ): CompletionResult { + val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { + Timber.e("Failed to create derivation path while first scan") + + return CompletionResult.Failure( + TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription), + ) + } + + val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath) + val derivationTaskResult = suspendCancellableCoroutine { continuation -> + derivationTask.run(session) { result -> + continuation.resume(result) + } + } + return handleDerivationResponse(derivationTaskResult, session) + } + + private suspend fun handleDerivationResponse( + result: CompletionResult, + session: CardSession, + ): CompletionResult { + return when (result) { + is CompletionResult.Success -> { + Timber.i("Start task for loading challenge for Visa wallet") + handleWalletAuthorization(session) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + + private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult { + Timber.i("Started handling authorization using Visa wallet") + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { + Timber.e("Failed to create derivation path while handling wallet authorization") + return CompletionResult.Failure( + TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription), + ) + } + + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { + Timber.e("Failed to find extended public key while handling wallet authorization") + return CompletionResult.Failure( + TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription), + ) + } + + val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { + Timber.e("Failed to find extended public key while handling wallet authorization") + return CompletionResult.Failure( + TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription), + ) + } + + Timber.i("Requesting challenge for wallet authorization") + // Will be changed later after backend implementation + val challengeResponse = runCatching { + visaAuthRepository.getCustomerWalletAuthChallenge( + cardId = card.cardId, + walletPublicKey = extendedPublicKey.publicKey.toHexString(), + ) + }.getOrElse { + return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error")) + } + + val signChallengeResult = signChallengeWithWallet( + publicKey = wallet.publicKey, + derivationPath = derivationPath, + nonce = challengeResponse.challenge, + session = session, + ) + + return when (signChallengeResult) { + is CompletionResult.Success -> { + Timber.i("Challenge signed with Wallet public key") + handleWalletAuthorizationTokens( + session = session, + signedChallenge = challengeResponse + .toSignedChallenge(signChallengeResult.data.signature.toHexString()), + ) + } + is CompletionResult.Failure -> { + Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}") + CompletionResult.Failure(signChallengeResult.error) + } + } + } + + private suspend fun handleWalletAuthorizationTokens( + session: CardSession, + signedChallenge: VisaAuthSignedChallenge, + ): CompletionResult { + val authorizationTokensResponse = runCatching { + visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) + }.getOrElse { + Timber.i( + "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", + ) + return handleCardAuthorization(session) + } + + visaAuthTokenStorage.store(authorizationTokensResponse) + + Timber.i("Authorized using Wallet public key successfully") + + return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) + } + + private suspend fun handleCardAuthorization(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + Timber.i("Requesting authorization challenge to sign") + + val challengeResponse = runCatching { + visaAuthRepository.getCardAuthChallenge( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey.toHexString(), + ) + }.getOrElse { + Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}") + return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error")) + } + + Timber.i("Received challenge to sign: ${challengeResponse.challenge}") + + val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge) + + val attestCardKeyResponse = when (signChallengeResult) { + is CompletionResult.Success -> { + Timber.i("Challenged signed.") + signChallengeResult.data + } + is CompletionResult.Failure -> { + Timber.e( + "Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}", + ) + return CompletionResult.Failure(signChallengeResult.error) + } + } + + @Suppress("UnusedPrivateMember") + val authorizationTokensResponse = runCatching { + visaAuthRepository.getAccessTokens( + signedChallenge = challengeResponse.toSignedChallenge( + signedChallenge = attestCardKeyResponse.cardSignature.toHexString(), + salt = attestCardKeyResponse.salt.toHexString(), + ), + ) + }.getOrElse { + Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}") + return CompletionResult.Failure( + TangemSdkError.Underlying( + customMessage = it.message ?: "Unknown error", + ), + ) + } + + visaAuthTokenStorage.store(authorizationTokensResponse) + + val activationRemoteState = visaActivationRepository.getActivationRemoteState() + + val error = when (activationRemoteState) { + VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation + VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState + else -> null + } + + if (error != null) { + return CompletionResult.Failure(TangemSdkError.Underlying(error.message)) + } + + val activationInput = VisaActivationInput( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey, + isAccessCodeSet = card.isAccessCodeSet, + ) + + return CompletionResult.Success( + VisaCardActivationStatus.ActivationStarted( + activationInput = activationInput, + authTokens = authorizationTokensResponse, + remoteState = activationRemoteState, + ), + ) + } + + private suspend fun signChallengeWithWallet( + publicKey: ByteArray, + derivationPath: DerivationPath, + nonce: String, + session: CardSession, + ): CompletionResult { + val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath) + val result = suspendCancellableCoroutine { + signHashCommand.run(session) { result -> + it.resume(result) + } + } + + return when (result) { + is CompletionResult.Success -> { + CompletionResult.Success(result.data) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } + + private suspend fun signChallengeWithCard( + session: CardSession, + challenge: String, + ): CompletionResult { + val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray()) + val result = suspendCancellableCoroutine { continuation -> + signHashCommand.run(session) { result -> + continuation.resume(result) + } + } + + return when (result) { + is CompletionResult.Success -> { + CompletionResult.Success(result.data) + } + is CompletionResult.Failure -> { + CompletionResult.Failure(result.error) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt new file mode 100644 index 0000000000..d507ac7b14 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.domain.visa + +private const val COMMON_DESCRIPTION = "Error occurred. Please contact support." + +internal enum class VisaCardScanHandlerError(val errorDescription: String) { + FailedToCreateDerivationPath(COMMON_DESCRIPTION), + FailedToFindWallet(COMMON_DESCRIPTION), + FailedToFindDerivedWalletKey(COMMON_DESCRIPTION), +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 713ffe5c9a..106bc50e1e 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 @@ -8,7 +8,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -18,6 +17,9 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay @@ -112,7 +114,7 @@ class DetailsMiddleware { private fun enrollBiometrics() { Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) - activityResultCaller.openSystemBiometrySettings() + store.inject(DaggerGraphState::settingsManager).openBiometricSettings() } private fun changeAppThemeMode(appThemeMode: AppThemeMode) { 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 fd477038f6..d4d46d362c 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 @@ -59,7 +59,6 @@ class WalletConnectMiddleware { is WalletConnectAction.StartWalletConnect -> { val uri = action.copiedUri if (uri != null && isWalletConnectUri(uri)) { - // TODO check store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index 168fccb7e7..c4cfcfd7ff 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -27,8 +26,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency import com.tangem.wallet.R import kotlinx.collections.immutable.ImmutableList @@ -114,7 +114,7 @@ private fun TopBar( is AppCurrencySelectorState.Loading, is AppCurrencySelectorState.Default, -> Text( - text = stringResource(id = R.string.details_row_title_currency), + text = stringResourceSafe(id = R.string.details_row_title_currency), style = TangemTheme.typography.subtitle1, ) } @@ -163,7 +163,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi singleLine = true, textStyle = TangemTheme.typography.subtitle2, placeholder = { - Text(text = stringResource(id = R.string.common_search)) + Text(text = stringResourceSafe(id = R.string.common_search)) }, colors = SearchBarColors, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index 06e802b5e0..29290619e6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components import android.content.res.Configuration import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog @@ -26,7 +26,7 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { onClick = dialog.onConfirm, ), dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = dialog.onDismiss, ), onDismissDialog = dialog.onDismiss, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 59af4ac24b..3dba72ed5d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components import android.content.res.Configuration import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog @@ -22,7 +22,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { selectedItemIndex = dialog.selectedItemIndex, items = dialog.items.map { it.resolveReference() }.toImmutableList(), confirmButton = DialogButtonUM( - title = stringResource(R.string.common_cancel), + title = stringResourceSafe(R.string.common_cancel), onClick = dialog.onDismiss, ), onSelect = dialog.onSelect, 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 index 57f1103703..c4454f830f 100644 --- 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 @@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.common.DetailsMainButton @@ -88,13 +88,13 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { ), ) { Text( - text = stringResource(id = R.string.scan_card_settings_title), + text = stringResourceSafe(id = R.string.scan_card_settings_title), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h3, ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) Text( - text = stringResource(id = R.string.scan_card_settings_message), + text = stringResourceSafe(id = R.string.scan_card_settings_message), color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body1, modifier = Modifier @@ -103,7 +103,7 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size32)) DetailsMainButton( - title = stringResource(id = R.string.scan_card_settings_button), + title = stringResourceSafe(id = R.string.scan_card_settings_button), onClick = onScanCardClick, ) } 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 index bbde2c8579..cbf005de4c 100644 --- 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 @@ -3,7 +3,7 @@ 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.core.ui.extensions.stringResourceSafe import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.securitymode.toTitleRes import com.tangem.wallet.R @@ -77,7 +77,7 @@ internal sealed interface TextReference { @ReadOnlyComposable internal fun TextReference.resolveReference(): String { return when (this) { - is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) + is TextReference.Res -> stringResourceSafe(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/coderecovery/AccessCodeRecoveryScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt index d8107e0c4d..8315639120 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement @@ -36,14 +36,14 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) { ) DetailsRadioButtonElement( - title = stringResource(id = R.string.common_enabled), - subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description), + title = stringResourceSafe(id = R.string.common_enabled), + subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_enabled_description), selected = state.enabledSelection, onClick = { state.onOptionClick(true) }, ) DetailsRadioButtonElement( - title = stringResource(id = R.string.common_disabled), - subtitle = stringResource(id = R.string.card_settings_access_code_recovery_disabled_description), + title = stringResourceSafe(id = R.string.common_disabled), + subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_disabled_description), selected = !state.enabledSelection, onClick = { state.onOptionClick(false) }, ) @@ -51,7 +51,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) { Spacer(modifier = Modifier.weight(1f)) DetailsMainButton( - title = stringResource(id = R.string.common_save_changes), + title = stringResourceSafe(id = R.string.common_save_changes), enabled = state.isSaveChangesEnabled, onClick = { state.onSaveChangesClick() }, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20), 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 index 8cdc6777a9..91558d9f57 100644 --- 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 @@ -10,9 +10,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.wallet.R @@ -64,7 +64,7 @@ internal fun SettingsScreensScaffold( ) { if (titleRes != null) { Text( - text = stringResource(id = titleRes), + text = stringResourceSafe(id = titleRes), modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing20) .padding(bottom = TangemTheme.dimens.spacing36), @@ -82,7 +82,7 @@ internal fun SettingsScreensScaffold( @Composable internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { Text( - text = stringResource(id = titleRes), + text = stringResourceSafe(id = titleRes), modifier = modifier.padding(start = 20.dp, end = 20.dp), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 6f35968117..f68ae0c367 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -10,9 +10,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.cardsettings.TextReference @@ -71,7 +71,7 @@ private fun ResetCardView(state: ResetCardScreenState) { @Composable private fun Title() { Text( - text = stringResource(id = R.string.card_settings_reset_card_to_factory), + text = stringResourceSafe(id = R.string.card_settings_reset_card_to_factory), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) @@ -89,7 +89,7 @@ private fun AlertImage() { @Composable private fun Subtitle() { Text( - text = stringResource(id = R.string.common_attention), + text = stringResourceSafe(id = R.string.common_attention), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) @@ -179,7 +179,7 @@ private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) { @Composable private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { DetailsMainButton( - title = stringResource(id = R.string.reset_card_to_factory_button_title), + title = stringResourceSafe(id = R.string.reset_card_to_factory_button_title), onClick = onResetButtonClick, enabled = enabled, ) @@ -188,14 +188,14 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { @Composable private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { BasicDialog( - title = stringResource(dialog.titleResId), - message = stringResource(dialog.messageResId), + title = stringResourceSafe(dialog.titleResId), + message = stringResourceSafe(dialog.messageResId), dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = dialog.onDismiss, ), confirmButton = DialogButtonUM( - title = stringResource(id = R.string.card_settings_action_sheet_reset), + title = stringResourceSafe(id = R.string.card_settings_action_sheet_reset), warning = true, onClick = dialog.onConfirmClick, ), @@ -206,10 +206,10 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { @Composable private fun CompletedResetDialog(dialog: ResetCardDialog) { BasicDialog( - title = stringResource(id = dialog.titleResId), - message = stringResource(id = dialog.messageResId), + title = stringResourceSafe(id = dialog.titleResId), + message = stringResourceSafe(id = dialog.messageResId), confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), onClick = dialog.onConfirmClick, ), onDismissDialog = {}, 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 index fe5164bf2f..3a86a4f67c 100644 --- 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 @@ -5,9 +5,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.stringResourceSafe 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.DetailsRadioButtonElement @@ -47,7 +47,7 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) { Spacer(modifier = Modifier.weight(1f)) DetailsMainButton( - title = stringResource(id = R.string.common_save_changes), + title = stringResourceSafe(id = R.string.common_save_changes), enabled = state.isSaveChangesEnabled, onClick = state.onSaveChangesClicked, modifier = Modifier.padding(start = 20.dp, end = 20.dp), @@ -68,8 +68,8 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat } DetailsRadioButtonElement( - title = stringResource(id = title), - subtitle = stringResource(id = subtitle), + title = stringResourceSafe(id = title), + subtitle = stringResourceSafe(id = subtitle), selected = selected, onClick = { state.onNewModeSelected(option) }, ) @@ -80,7 +80,7 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat private fun SecurityModeScreenPreview() { SecurityModeScreen( state = SecurityModeScreenState( - availableOptions = SecurityOption.values().toList(), + availableOptions = SecurityOption.entries, selectedSecurityMode = SecurityOption.LongTap, isSaveChangesEnabled = false, onNewModeSelected = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index 7e5b3542cd..b25fe77799 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -5,21 +5,19 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material3.* 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.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R import kotlinx.collections.immutable.persistentListOf @@ -30,8 +28,6 @@ internal fun WalletConnectScreen( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - SettingsScreensScaffold( modifier = modifier, content = { @@ -46,7 +42,7 @@ internal fun WalletConnectScreen( AddSessionFab( onAddSession = { Analytics.send(Settings.ButtonStartWalletConnectSession()) - state.onAddSession(context.getFromClipboard()?.toString()) + state.onAddSession() }, ) } @@ -60,7 +56,7 @@ internal fun WalletConnectScreen( private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) { FloatingActionButton( onClick = onAddSession, - backgroundColor = TangemTheme.colors.button.primary, + containerColor = TangemTheme.colors.button.primary, contentColor = TangemTheme.colors.icon.primary2, shape = RoundedCornerShape(16.dp), modifier = modifier, @@ -96,7 +92,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { ) Spacer(modifier = Modifier.size(24.dp)) Text( - text = stringResource(id = R.string.wallet_connect_subtitle), + text = stringResourceSafe(id = R.string.wallet_connect_subtitle), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) 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 index a9a526616d..0a3d429cd6 100644 --- 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 @@ -6,7 +6,7 @@ internal data class WalletConnectScreenState( val sessions: ImmutableList, val isLoading: Boolean = false, val onRemoveSession: (String) -> Unit = {}, - val onAddSession: (String?) -> Unit = {}, + val onAddSession: () -> Unit = {}, ) data class WcSessionForScreen( 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 index 284583f633..2ee11076ee 100644 --- 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 @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect import androidx.lifecycle.* import arrow.core.getOrElse +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -17,6 +18,7 @@ import javax.inject.Inject @HiltViewModel internal class WalletConnectViewModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val clipboardManager: ClipboardManager, ) : ViewModel(), DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { @@ -35,7 +37,11 @@ internal class WalletConnectViewModel @Inject constructor( sessions.toImmutableList(), isLoading = state.loading, onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) }, - onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, + onAddSession = { + store.dispatch( + WalletConnectAction.StartWalletConnect(copiedUri = clipboardManager.getText()), + ) + }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index f33229cf24..88f9d53d3b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -57,6 +57,7 @@ internal fun StoriesScreen( } } + // todo refactor [REDACTED_TASK_KEY] StoriesScreenContent( modifier = Modifier .fillMaxSize() @@ -75,6 +76,7 @@ internal fun StoriesScreen( ) } +@Deprecated("Use StoriesContainer from core/ui") @Suppress("LongMethod") @Composable private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) { 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 index 198f6930da..0a1f0fdaed 100644 --- 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 @@ -2,20 +2,23 @@ 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.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation @@ -27,8 +30,8 @@ fun StoriesRevolutionaryWallet() { SplitContent( topContent = { TopContent( - titleText = stringResource(id = R.string.story_awe_title), - subtitleText = stringResource(id = R.string.story_awe_description), + titleText = stringResourceSafe(id = R.string.story_awe_title), + subtitleText = stringResourceSafe(id = R.string.story_awe_description), ) }, bottomContent = { @@ -46,8 +49,8 @@ fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) { SplitContent( topContent = { TopContent( - titleText = stringResource(id = R.string.story_backup_title), - subtitleText = stringResource(id = R.string.story_backup_description), + titleText = stringResourceSafe(id = R.string.story_backup_title), + subtitleText = stringResourceSafe(id = R.string.story_backup_description), ) }, bottomContent = { @@ -65,8 +68,8 @@ 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), + titleText = stringResourceSafe(id = R.string.story_currencies_title), + subtitleText = stringResourceSafe(id = R.string.story_currencies_description), ) }, bottomContent = { @@ -81,8 +84,8 @@ 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), + titleText = stringResourceSafe(id = R.string.story_web3_title), + subtitleText = stringResourceSafe(id = R.string.story_web3_description), ) }, bottomContent = { @@ -97,8 +100,8 @@ fun StoriesWalletForEveryone(stepDuration: Int) { SplitContent( topContent = { TopContent( - titleText = stringResource(id = R.string.story_finish_title), - subtitleText = stringResource(id = R.string.story_finish_description), + titleText = stringResourceSafe(id = R.string.story_finish_title), + subtitleText = stringResourceSafe(id = R.string.story_finish_description), ) }, bottomContent = { 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 index 1577eecad8..184bf8d37d 100644 --- 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 @@ -16,13 +16,13 @@ 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.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.sp import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.home.compose.StoriesTextAnimation @@ -66,7 +66,7 @@ fun FirstStoriesContent(isPaused: Boolean, duration: Int) { ) { modifier -> Text( modifier = modifier, - text = stringResource(R.string.story_meet_title), + text = stringResourceSafe(R.string.story_meet_title), style = style, color = TangemColorPalette.White, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index be6823d684..6b7987848b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -9,12 +9,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags @@ -52,7 +52,7 @@ internal fun HomeButtons( private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { StoriesButton( modifier = modifier, - text = stringResource(id = R.string.home_button_scan), + text = stringResourceSafe(id = R.string.home_button_scan), useDarkerColors = false, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), onClick = onClick, @@ -64,7 +64,7 @@ private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) { StoriesButton( modifier = modifier, - text = stringResource(id = R.string.home_button_order), + text = stringResourceSafe(id = R.string.home_button_order), useDarkerColors = true, onClick = onClick, ) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt index e3e237954e..4d70d5f2f6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt @@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @@ -18,7 +18,7 @@ import com.tangem.wallet.R internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) { StoriesButton( modifier = modifier, - text = stringResource(id = R.string.common_search_tokens), + text = stringResourceSafe(id = R.string.common_search_tokens), icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24), showProgress = false, useDarkerColors = true, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt index c2e879dc2d..66fc2bd6f1 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt @@ -1,10 +1,9 @@ package com.tangem.tap.features.home.compose.views -import androidx.compose.material.ButtonColors +import androidx.compose.material3.ButtonColors import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -37,16 +36,16 @@ internal fun StoriesButton( ) } -private val LighterButtonColors: ButtonColors = TangemButtonColors( - backgroundColor = TangemColorPalette.Light4, +private val LighterButtonColors: ButtonColors = ButtonColors( + containerColor = TangemColorPalette.Light4, contentColor = TangemColorPalette.Dark6, - disabledBackgroundColor = TangemColorPalette.Dark5, + disabledContainerColor = TangemColorPalette.Dark5, disabledContentColor = TangemColorPalette.Dark6, ) -private val DarkerButtonColors: ButtonColors = TangemButtonColors( - backgroundColor = TangemColorPalette.Dark4, +private val DarkerButtonColors: ButtonColors = ButtonColors( + containerColor = TangemColorPalette.Dark4, contentColor = TangemColorPalette.White, - disabledBackgroundColor = TangemColorPalette.Dark4, + disabledContainerColor = TangemColorPalette.Dark4, disabledContentColor = TangemColorPalette.White, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/data/HomeRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/features/home/data/HomeRepositoryImpl.kt deleted file mode 100644 index fed3f2e394..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/data/HomeRepositoryImpl.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.home.data - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.GeoResponse -import com.tangem.tap.features.home.domain.HomeRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -/** - * Implementation of repository for Home feature - * - * @property tangemTechApi API for server requests - * @property dispatchers coroutine dispatcher provider - */ -class HomeRepositoryImpl( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) : HomeRepository { - - override suspend fun getUserCountryCode(): GeoResponse = withContext(dispatchers.io) { - tangemTechApi.getUserCountryCode() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/domain/HomeRepository.kt b/app/src/main/java/com/tangem/tap/features/home/domain/HomeRepository.kt deleted file mode 100644 index ad489ae0fe..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/domain/HomeRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.home.domain - -import com.tangem.datasource.api.tangemTech.models.GeoResponse - -/** Repository for Home feature */ -interface HomeRepository { - - /** Get user's country code */ - suspend fun getUserCountryCode(): GeoResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt deleted file mode 100644 index 55ebe3e10d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.home.featuretoggles - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import javax.inject.Inject - -class HomeFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) { - - val isMigrateUserCountryCodeEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "MIGRATE_USER_COUNTRY_CODE_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 87a615ef97..ffe384bee5 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -53,7 +53,6 @@ private fun handleHomeAction(action: Action) { store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) - store.dispatch(GlobalAction.FetchUserCountry) } is HomeAction.ReadCard -> { action.scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 5341631157..89d3c728a6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import org.rekotlin.StateType +// todo refactor [REDACTED_TASK_KEY] data class HomeState( val scanInProgress: Boolean = false, val stories: ImmutableList = Stories.entries.toImmutableList(), diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/AffectsNavigation.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/AffectsNavigation.kt new file mode 100644 index 0000000000..e6d38c381c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/AffectsNavigation.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.features.intentHandler + +interface AffectsNavigation \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt index 8ca7a84a1b..6883c38214 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt @@ -19,9 +19,9 @@ class IntentProcessor { intentHandlers.clear() } - fun handleIntent(intent: Intent?, isFromForeground: Boolean) { - intentHandlers.forEach { - it.handleIntent(intent, isFromForeground) - } + fun handleIntent(intent: Intent?, isFromForeground: Boolean, skipNavigationHandlers: Boolean = false) { + intentHandlers + .filterNot { handler -> skipNavigationHandlers && handler is AffectsNavigation } + .forEach { handler -> handler.handleIntent(intent, isFromForeground) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 14c831cc90..90ef042a6b 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -7,6 +7,7 @@ import android.os.Build import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.intentHandler.IntentHandler +import com.tangem.tap.features.intentHandler.AffectsNavigation import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.store import kotlinx.coroutines.CoroutineScope @@ -17,7 +18,7 @@ import kotlinx.coroutines.CoroutineScope class BackgroundScanIntentHandler( private val hasSavedUserWalletsProvider: () -> Boolean, private val scope: CoroutineScope, -) : IntentHandler { +) : IntentHandler, AffectsNavigation { private val nfcActions = arrayOf( NfcAdapter.ACTION_NDEF_DISCOVERED, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 273ca6d3b2..6f138c18e5 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -5,6 +5,7 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.intentHandler.IntentHandler +import com.tangem.tap.features.intentHandler.AffectsNavigation import com.tangem.tap.store import timber.log.Timber import java.net.URLDecoder @@ -12,7 +13,7 @@ import java.net.URLDecoder /** [REDACTED_AUTHOR] */ -class WalletConnectLinkIntentHandler : IntentHandler { +class WalletConnectLinkIntentHandler : IntentHandler, AffectsNavigation { override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { val intentData = intent?.data ?: return false diff --git a/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt b/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt deleted file mode 100644 index bfbfb42379..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.main - -internal interface MainIntents { - - fun onHiddenBalanceToastAction() - - fun onShownBalanceToastAction() - - fun onHiddenBalanceNotificationAction(isPermanent: Boolean) - - fun onDismissBottomSheet() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt deleted file mode 100644 index 9bbeded200..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.tap.features.main - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.tap.features.main.model.MainScreenState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -internal class MainScreenStateHolder(private val intents: MainIntents) { - - private val notificationsFactory = NotificationsFactory(intents) - - private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) - - val stateFlow: StateFlow = stateFlowInternal - - fun updateWithHiddenBalancesToast(isBalanceHidden: Boolean) { - stateFlowInternal.update { state -> - state.copy( - toast = triggeredEvent( - data = if (isBalanceHidden) { - notificationsFactory.createBalancesAreHiddenToast() - } else { - notificationsFactory.createBalancesAreShownToast() - }, - onConsume = ::consumeToastEvent, - ), - ) - } - } - - fun updateWithHiddenBalancesNotification() { - stateFlowInternal.update { state -> - state.copy( - modalNotification = TangemBottomSheetConfig( - isShow = true, - onDismissRequest = intents::onDismissBottomSheet, - content = notificationsFactory.createBalancesAreHiddenModalNotification(), - ), - ) - } - } - - fun updateWithoutModalNotification() { - stateFlowInternal.update { state -> - state.copy(modalNotification = state.modalNotification?.copy(isShow = false)) - } - } - - private fun consumeToastEvent() { - stateFlowInternal.update { state -> - state.copy(toast = consumedEvent()) - } - } - - private fun getInitialState() = MainScreenState( - toast = consumedEvent(), - modalNotification = null, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index d4f05a284a..83fb5f942c 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -3,9 +3,17 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter +import com.tangem.common.keyboard.KeyboardValidator import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.TechAnalyticsEvent +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.BottomSheetMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings @@ -18,8 +26,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.extensions.setContext -import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles -import com.tangem.tap.features.main.model.MainScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -32,7 +38,6 @@ import javax.inject.Inject internal class MainViewModel @Inject constructor( private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, - private val router: AppRouter, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, @@ -41,20 +46,16 @@ internal class MainViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val apiConfigsManager: ApiConfigsManager, - homeFeatureToggles: HomeFeatureToggles, private val fetchUserCountryUseCase: FetchUserCountryUseCase, + @GlobalUiMessageSender private val messageSender: UiMessageSender, + private val keyboardValidator: KeyboardValidator, + private val analyticsEventHandler: AnalyticsEventHandler, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, -) : ViewModel(), MainIntents { - - private val stateHolder = MainScreenStateHolder( - intents = this, - ) +) : ViewModel() { private val balanceHidingSettingsFlow: SharedFlow = getBalanceHidingSettingsUseCase() .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) - val state: StateFlow = stateHolder.stateFlow - var isSplashScreenShown: Boolean = true private set @@ -63,11 +64,9 @@ internal class MainViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } - if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) { - viewModelScope.launch { - fetchUserCountryUseCase().onLeft { - Timber.e("Unable to fetch the user country code $it") - } + viewModelScope.launch { + fetchUserCountryUseCase().onLeft { + Timber.e("Unable to fetch the user country code $it") } } @@ -79,6 +78,8 @@ internal class MainViewModel @Inject constructor( fetchStakingTokens() deleteDeprecatedLogsUseCase() + + sendKeyboardIdentifierEvent() } /** Loading the resources needed to run the application */ @@ -129,10 +130,34 @@ internal class MainViewModel @Inject constructor( it.isBalanceHidingNotificationEnabled && it.isBalanceHidden } .onEach { - if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) { + if (!it.isUpdateFromToast) { listenToFlipsUseCase.changeUpdateEnabled(false) - stateHolder.updateWithHiddenBalancesNotification() - router.push(AppRoute.ModalNotification) + + val message = BottomSheetMessage.invoke( + iconResId = R.drawable.ic_eye_off_outline_24, + title = resourceReference(R.string.balance_hidden_title), + message = resourceReference(R.string.balance_hidden_description), + onDismissRequest = ::onBottomSheetDismissed, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.balance_hidden_got_it_button), + onClick = { + onHiddenBalanceNotificationAction(isPermanent = false) + onDismissRequest() + }, + ) + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.balance_hidden_do_not_show_button), + onClick = { + onHiddenBalanceNotificationAction(isPermanent = true) + onDismissRequest() + }, + ) + }, + ) + messageSender.send(message) } } .launchIn(viewModelScope) @@ -171,11 +196,27 @@ internal class MainViewModel @Inject constructor( private fun displayBalancesHiddenStatusToast(settings: BalanceHidingSettings) { // If modal notification is enabled and balances are hidden, the toast will not show if (!settings.isBalanceHidingNotificationEnabled || !settings.isBalanceHidden) { - stateHolder.updateWithHiddenBalancesToast(settings.isBalanceHidden) + val message = SnackbarMessage( + message = if (settings.isBalanceHidden) { + resourceReference(R.string.toast_balances_hidden) + } else { + resourceReference(R.string.toast_balances_shown) + }, + actionLabel = resourceReference(R.string.toast_undo), + action = { + if (settings.isBalanceHidden) { + onHiddenBalanceToastAction() + } else { + onShownBalanceToastAction() + } + }, + ) + + messageSender.send(message) } } - override fun onHiddenBalanceToastAction() { + private fun onHiddenBalanceToastAction() { viewModelScope.launch { updateBalanceHidingSettingsUseCase.invoke { copy( @@ -186,7 +227,7 @@ internal class MainViewModel @Inject constructor( } } - override fun onShownBalanceToastAction() { + private fun onShownBalanceToastAction() { viewModelScope.launch { updateBalanceHidingSettingsUseCase.invoke { copy( @@ -197,10 +238,14 @@ internal class MainViewModel @Inject constructor( } } - override fun onHiddenBalanceNotificationAction(isPermanent: Boolean) { - onDismissBottomSheet() + private fun onHiddenBalanceNotificationAction(isPermanent: Boolean) { + val message = SnackbarMessage( + message = resourceReference(R.string.toast_balances_hidden), + actionLabel = resourceReference(R.string.toast_undo), + action = ::onHiddenBalanceToastAction, + ) + messageSender.send(message) - stateHolder.updateWithHiddenBalancesToast(true) if (isPermanent) { viewModelScope.launch { updateBalanceHidingSettingsUseCase.invoke { @@ -210,10 +255,27 @@ internal class MainViewModel @Inject constructor( } } - override fun onDismissBottomSheet() { - listenToFlipsUseCase.changeUpdateEnabled(true) - router.pop() - stateHolder.updateWithoutModalNotification() - stateHolder.updateWithHiddenBalancesToast(true) + private fun onBottomSheetDismissed() { + listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true) + } + + private fun sendKeyboardIdentifierEvent() { + viewModelScope.launch { + val keyboardId = keyboardValidator.getKeyboardId() + + if (keyboardId != null) { + Timber.d("Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}") + + analyticsEventHandler.send( + event = TechAnalyticsEvent.KeyboardIdentifier( + id = keyboardId.value, + packageName = keyboardId.getPackageName(), + isTrusted = keyboardValidator.validate(id = keyboardId), + ), + ) + } else { + Timber.e("Unable to get keyboard identifier") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt b/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt deleted file mode 100644 index cc2dcc6c40..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.tap.features.main - -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.tap.features.main.model.ActionConfig -import com.tangem.tap.features.main.model.ModalNotification -import com.tangem.tap.features.main.model.Toast -import com.tangem.wallet.R - -internal class NotificationsFactory( - private val intents: MainIntents, -) { - - fun createBalancesAreHiddenToast(): Toast { - return Toast( - message = resourceReference(R.string.toast_balances_hidden), - action = ActionConfig( - text = resourceReference(R.string.toast_undo), - onClick = intents::onHiddenBalanceToastAction, - ), - ) - } - - fun createBalancesAreShownToast(): Toast { - return Toast( - message = resourceReference(R.string.toast_balances_shown), - action = ActionConfig( - text = resourceReference(R.string.toast_undo), - onClick = intents::onShownBalanceToastAction, - ), - ) - } - - fun createBalancesAreHiddenModalNotification(): ModalNotification { - return ModalNotification( - iconResId = R.drawable.ic_eye_off_outline_24, - title = resourceReference(R.string.balance_hidden_title), - message = resourceReference(R.string.balance_hidden_description), - primaryAction = ActionConfig( - text = resourceReference(R.string.balance_hidden_got_it_button), - onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = false) }, - ), - secondaryAction = ActionConfig( - text = resourceReference(R.string.balance_hidden_do_not_show_button), - onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = true) }, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt b/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt deleted file mode 100644 index 6549c35d88..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.features.main.model - -import com.tangem.core.ui.extensions.TextReference - -internal data class ActionConfig( - val text: TextReference, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt b/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt deleted file mode 100644 index 7a787bbc7b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.main.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent - -internal data class MainScreenState( - val toast: StateEvent, - val modalNotification: TangemBottomSheetConfig?, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt b/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt deleted file mode 100644 index eaf3d37833..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.features.main.model - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class ModalNotification( - @DrawableRes val iconResId: Int, - val title: TextReference, - val message: TextReference, - val primaryAction: ActionConfig, - val secondaryAction: ActionConfig?, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt b/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt deleted file mode 100644 index 9e959eb892..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.features.main.model - -import com.tangem.core.ui.extensions.TextReference - -internal data class Toast( - val message: TextReference, - val action: ActionConfig, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt deleted file mode 100644 index ef72720cad..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.tap.features.main.ui - -import android.content.DialogInterface -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.tap.features.main.MainViewModel -import com.tangem.tap.features.main.model.ModalNotification -import com.tangem.tap.features.main.ui.components.ModalNotificationContent -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - private val viewModel: MainViewModel by activityViewModels() - - @Composable - override fun ScreenContent(modifier: Modifier) { - val state by viewModel.state.collectAsStateWithLifecycle() - val notification = state.modalNotification - if (notification == null) { - dismiss() - return - } - - BackHandler(onBack = notification.onDismissRequest) - - when (val content = notification.content) { - is ModalNotification -> ModalNotificationContent( - modifier = modifier - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - notification = content, - ) - else -> Unit - } - - LaunchedEffect(notification) { - if (!notification.isShow) { - dismiss() - } - } - } - - override fun onDismiss(dialog: DialogInterface) { - viewModel.state.value.modalNotification?.onDismissRequest?.invoke() - super.onDismiss(dialog) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt deleted file mode 100644 index 72c36caf54..0000000000 --- a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.tangem.tap.features.main.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -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.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.main.model.ActionConfig -import com.tangem.tap.features.main.model.ModalNotification -import com.tangem.wallet.R - -@Composable -internal fun ModalNotificationContent(notification: ModalNotification, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing40), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Hand() - Icon( - modifier = Modifier.size(TangemTheme.dimens.size48), - painter = painterResource(notification.iconResId), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = notification.title.resolveReference(), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Text( - text = notification.message.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = notification.primaryAction.text.resolveReference(), - onClick = notification.primaryAction.onClick, - ) - when (val secondaryAction = notification.secondaryAction) { - null -> Unit - else -> SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = secondaryAction.text.resolveReference(), - onClick = secondaryAction.onClick, - ) - } - } - } -} - -// region Preview -@Preview(widthDp = 360, heightDp = 404) -@Preview(widthDp = 360, heightDp = 404, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ModalNotificationContentPreview( - @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, -) { - TangemThemePreview { - ModalNotificationContent( - notification = param, - modifier = Modifier.background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - ) - } -} - -private class GlobalNotificationProvider : CollectionPreviewParameterProvider( - collection = listOf( - ModalNotification( - iconResId = R.drawable.ic_eye_off_outline_24, - title = resourceReference(R.string.balance_hidden_title), - message = resourceReference(R.string.balance_hidden_description), - primaryAction = ActionConfig( - text = resourceReference(R.string.balance_hidden_got_it_button), - onClick = {}, - ), - secondaryAction = ActionConfig( - text = resourceReference(R.string.balance_hidden_do_not_show_button), - onClick = {}, - ), - ), - ), -) -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index e6166576ce..796b99ce27 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -22,11 +22,9 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState @@ -50,7 +48,7 @@ object OnboardingHelper { if (!response.twinsIsTwinned()) { true } else { - onboardingManager.isActivationInProgress(cardId) ?: false + onboardingManager.isActivationInProgress(cardId) } } @@ -62,7 +60,7 @@ object OnboardingHelper { emptyWallets || activationInProgress || isNoBackup } - response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false + response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) else -> true } } @@ -70,7 +68,7 @@ object OnboardingHelper { fun whereToNavigate(scanResponse: ScanResponse): AppRoute { val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring || - scanResponse.productType == ProductType.Wallet + scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled && newOnboardingSupportTypes ) { @@ -222,7 +220,7 @@ object OnboardingHelper { } } - fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) { + fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) { val blockchain = walletManager.wallet.blockchain val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains) @@ -238,17 +236,10 @@ object OnboardingHelper { Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) scope.launch { - val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles) + val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase) + val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia } + val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles) - - val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) { - val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase) - - getUserCountryCodeUseCase().isRight { it is UserCountry.Russia } - } else { - globalState.userCountryCode == RUSSIA_COUNTRY_CODE - } - if (isRussia && !onrampFeatureToggles.isFeatureEnabled) { val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl) store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData)) 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 d47862a648..99c704189c 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 @@ -176,7 +176,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch OnboardingHelper.handleTopUpAction( walletManager = walletManager, scanResponse = scanResponse, - globalState = globalState, ) } is OnboardingNoteAction.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 ef29db32d4..37ecb6d41f 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 @@ -299,7 +299,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) { OnboardingHelper.handleTopUpAction( walletManager = walletManager, scanResponse = onboardingManager?.scanResponse ?: return, - globalState = globalState, ) } TwinCardsAction.Done -> { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 7f20094f7b..60ecd8a44e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -37,7 +37,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.extensions.setStatusBarColor -import com.tangem.core.ui.message.EventMessageEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.windowsize.rememberWindowSize import com.tangem.datasource.utils.isNullOrEmpty @@ -301,10 +300,6 @@ class OnboardingWalletFragment : windowSize = rememberWindowSize(activity = requireActivity()), ) { onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize()) - EventMessageEffect( - messageHandler = uiDependencies.eventMessageHandler, - snackbarHostState = uiDependencies.globalSnackbarHostState, - ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 2c7903ad60..cc39db075a 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -8,7 +8,6 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -17,7 +16,11 @@ import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch @@ -97,7 +100,7 @@ internal class SaveWalletMiddleware { } private fun enrollBiometrics() { - activityResultCaller.openSystemBiometrySettings() + store.inject(DaggerGraphState::settingsManager).openBiometricSettings() } private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 1f3b36e57d..54da091df6 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -5,11 +5,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @@ -17,10 +17,10 @@ import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Composable fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) { BasicDialog( - title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title), - message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description), + title = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_title), + message = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_description), confirmButton = DialogButtonUM( - title = stringResource(R.string.common_enable), + title = stringResourceSafe(R.string.common_enable), onClick = dialog.onEnroll, ), dismissButton = DialogButtonUM( diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index 4206df1904..c23f77abe1 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @@ -63,7 +63,7 @@ private fun Header(onCloseClick: () -> Unit) { modifier = Modifier.size(TangemTheme.dimens.size24), painter = painterResource(id = R.drawable.ic_close_24), tint = TangemTheme.colors.icon.secondary, - contentDescription = stringResource(id = R.string.common_cancel), + contentDescription = stringResourceSafe(id = R.string.common_cancel), ) } SpacerW8() @@ -85,7 +85,7 @@ private fun Title(modifier: Modifier = Modifier) { contentDescription = null, ) Text( - text = stringResource(id = R.string.save_user_wallet_agreement_header_biometrics), + text = stringResourceSafe(id = R.string.save_user_wallet_agreement_header_biometrics), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -102,13 +102,13 @@ private fun Description(modifier: Modifier = Modifier) { ) { DescriptionItem( iconPainter = painterResource(id = R.drawable.ic_face_recognition_24), - title = stringResource(id = R.string.save_user_wallet_agreement_access_title), - description = stringResource(id = R.string.save_user_wallet_agreement_access_description), + title = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_title), + description = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_description), ) DescriptionItem( iconPainter = painterResource(id = R.drawable.ic_lock_24), - title = stringResource(id = R.string.save_user_wallet_agreement_code_title), - description = stringResource(id = R.string.save_user_wallet_agreement_code_description_biometrics), + title = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_title), + description = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_description_biometrics), ) } } @@ -125,12 +125,12 @@ private fun Footer(showProgress: Boolean, onSaveWalletClick: () -> Unit) { PrimaryButton( modifier = Modifier.fillMaxWidth(), showProgress = showProgress, - text = stringResource(id = R.string.save_user_wallet_agreement_allow_biometrics), + text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics), onClick = onSaveWalletClick, ) Text( modifier = Modifier.fillMaxWidth(fraction = .7f), - text = stringResource(R.string.save_user_wallet_agreement_notice), + text = stringResourceSafe(R.string.save_user_wallet_agreement_notice), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, 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 5b5d56d9d3..6c9ed1d2e8 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 @@ -17,7 +17,6 @@ import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.proxy.redux.DaggerGraphState @@ -26,7 +25,6 @@ import com.tangem.tap.store import kotlinx.coroutines.launch import org.rekotlin.Middleware -@Suppress("LargeClass") @Deprecated("Will be removed soon") object TradeCryptoMiddleware { @@ -41,23 +39,22 @@ object TradeCryptoMiddleware { } } - @Suppress("LongMethod", "CyclomaticComplexMethod") private fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return when (action) { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.Buy -> proceedBuyAction(state, action) + is TradeCryptoAction.Buy -> proceedBuyAction(action) is TradeCryptoAction.Sell -> proceedSellAction(action) } } - private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { + private fun proceedBuyAction(action: TradeCryptoAction.Buy) { val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled if (isOnrampEnabled) { proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency, action.source) } else { - proceedWithLegacyBuyAction(state, action) + proceedWithLegacyBuyAction(action) } } @@ -73,7 +70,7 @@ object TradeCryptoMiddleware { } } - private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { + private fun proceedWithLegacyBuyAction(action: TradeCryptoAction.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) @@ -92,15 +89,10 @@ object TradeCryptoMiddleware { ) scope.launch { - val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles) + val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase) val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles) - val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) { - val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase) - getUserCountryCodeUseCase().isRight { it is UserCountry.Russia } - } else { - state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE - } + val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia } if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) { val dialogData = topUrl?.let { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt new file mode 100644 index 0000000000..b90ec7a20f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.welcome.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.tap.features.welcome.model.WelcomeModel +import com.tangem.tap.features.welcome.ui.components.WelcomeScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultWelcomeComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: WelcomeComponent.Params, +) : WelcomeComponent, AppComponentContext by context { + + private val model: WelcomeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + WelcomeScreen( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : WelcomeComponent.Factory { + override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt new file mode 100644 index 0000000000..07a92373e1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.welcome.component + +import com.tangem.common.routing.entity.SerializableIntent +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface WelcomeComponent : ComposableContentComponent { + + data class Params( + val intent: SerializableIntent?, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt new file mode 100644 index 0000000000..94c08e4f70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.welcome.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.tap.features.welcome.component.WelcomeComponent +import com.tangem.tap.features.welcome.ui.WelcomeScreenState +import com.tangem.tap.features.welcome.ui.components.WelcomeScreen + +internal class PreviewWelcomeComponent( + private val initialState: WelcomeScreenState = WelcomeScreenState(), +) : WelcomeComponent { + + @Composable + override fun Content(modifier: Modifier) { + WelcomeScreen( + modifier = modifier, + state = initialState, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt new file mode 100644 index 0000000000..68f1c6121f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.welcome.di + +import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent +import com.tangem.tap.features.welcome.component.WelcomeComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt new file mode 100644 index 0000000000..1a90bd0bc5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.welcome.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.tap.features.welcome.model.WelcomeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(WelcomeModel::class) + fun bindWelcomeModel(model: WelcomeModel): Model +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt new file mode 100644 index 0000000000..143654b18d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -0,0 +1,137 @@ +package com.tangem.tap.features.welcome.model + +import com.tangem.common.core.TangemError +import com.tangem.core.analytics.Analytics +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.tap.common.analytics.events.SignIn +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.welcome.component.WelcomeComponent +import com.tangem.tap.features.welcome.redux.WelcomeAction +import com.tangem.tap.features.welcome.redux.WelcomeState +import com.tangem.tap.features.welcome.ui.WelcomeScreenState +import com.tangem.tap.features.welcome.ui.model.WarningModel +import com.tangem.tap.store +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +// FIXME: Remove redux: [REDACTED_JIRA] +internal class WelcomeModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val appFinisher: AppFinisher, + paramsContainer: ParamsContainer, +) : Model(), StoreSubscriber { + + private val params: WelcomeComponent.Params = paramsContainer.require() + private val initialState: WelcomeScreenState = WelcomeScreenState( + onPopBack = appFinisher::finish, + onUnlockClick = this::unlockWallets, + onScanCardClick = this::scanCard, + onCloseError = this::closeError, + ) + + val state: MutableStateFlow = MutableStateFlow(initialState) + + init { + store.dispatch(WelcomeAction.SetCoroutineScope(modelScope)) + + subscribeToStoreChanges() + initGlobalState() + + val welcomeAction = if (params.intent != null) { + WelcomeAction.ProceedWithIntent(params.intent.toIntent()) + } else { + WelcomeAction.ProceedWithBiometrics() + } + + store.dispatch(welcomeAction) + } + + private fun unlockWallets() { + Analytics.send(SignIn.ButtonBiometricSignIn()) + store.dispatch(WelcomeAction.ProceedWithBiometrics()) + } + + private fun scanCard() { + Analytics.send(SignIn.ButtonCardSignIn()) + store.dispatch(WelcomeAction.ProceedWithCard) + } + + private fun closeError() { + store.dispatch(WelcomeAction.CloseError) + } + + override fun newState(state: WelcomeState) { + val warning = createWarningIfNeeded(state.error) + + this.state.update { prevState -> + prevState.copy( + showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress, + showUnlockWithCardProgress = state.isUnlockWithCardInProgress, + warning = warning, + error = state.error + ?.takeIf { !it.silent && warning == null } + ?.let { e -> + e.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(e.customMessage) + }, + ) + } + } + + override fun onDestroy() { + store.dispatch(WelcomeAction.ClearCoroutineScope) + store.unsubscribe(subscriber = this) + + super.onDestroy() + } + + private fun createWarningIfNeeded(error: TangemError?): WarningModel? { + return when (error) { + is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning( + isPermanent = error.isPermanent, + onDismiss = this::dismissWarning, + ) + is UserWalletsListError.AllKeysInvalidated, + is UserWalletsListError.NoUserWalletSelected, + -> WarningModel.KeyInvalidatedWarning( + onDismiss = this::dismissWarning, + ) + is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning( + onDismiss = this::clearUserWallets, + ) + else -> null + } + } + + private fun dismissWarning() { + state.update { prevState -> + prevState.copy( + warning = null, + ) + } + closeError() + } + + private fun clearUserWallets() { + store.dispatch(WelcomeAction.ClearUserWallets) + } + + private fun subscribeToStoreChanges() { + store.subscribe(this) { appState -> + appState.skip { old, new -> old.welcomeState == new.welcomeState } + .select { it.welcomeState } + } + } + + private fun initGlobalState() { + store.dispatch(GlobalAction.RestoreAppCurrency) + store.dispatch(GlobalAction.ExchangeManager.Init) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 701042816e..3ede2ae446 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -1,13 +1,15 @@ package com.tangem.tap.features.welcome.ui -import androidx.compose.runtime.Immutable import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.welcome.ui.model.WarningModel -@Immutable internal data class WelcomeScreenState( + val onPopBack: () -> Unit = {}, val showUnlockWithBiometricsProgress: Boolean = false, val showUnlockWithCardProgress: Boolean = false, val warning: WarningModel? = null, val error: TextReference? = null, + val onUnlockClick: () -> Unit = {}, + val onScanCardClick: () -> Unit = {}, + val onCloseError: () -> Unit = {}, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 0ecd71bd5d..592678f735 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -130,6 +130,5 @@ internal class WelcomeViewModel @Inject constructor( private fun initGlobalState() { store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) - store.dispatch(GlobalAction.FetchUserCountry) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index 57f226a9d7..2dc63dd5dd 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -4,10 +4,10 @@ import android.content.res.Configuration import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -18,8 +18,8 @@ internal fun WarningDialog(warning: WarningModel?) { null -> Unit is WarningModel.BiometricsLockoutWarning -> { BasicDialog( - title = stringResource(id = R.string.biometric_lockout_warning_title), - message = stringResource( + title = stringResourceSafe(id = R.string.biometric_lockout_warning_title), + message = stringResourceSafe( id = if (warning.isPermanent) { R.string.biometric_lockout_permanent_warning_description } else { @@ -28,30 +28,30 @@ internal fun WarningDialog(warning: WarningModel?) { ), onDismissDialog = warning.onDismiss, confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), onClick = warning.onDismiss, ), ) } is WarningModel.KeyInvalidatedWarning -> { BasicDialog( - title = stringResource(id = R.string.common_attention), - message = stringResource(id = R.string.key_invalidated_warning_description), + title = stringResourceSafe(id = R.string.common_attention), + message = stringResourceSafe(id = R.string.key_invalidated_warning_description), onDismissDialog = warning.onDismiss, confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), onClick = warning.onDismiss, ), ) } is WarningModel.BiometricsDisabledWarning -> { BasicDialog( - title = stringResource(id = R.string.common_warning), - message = stringResource(id = R.string.biometric_unavailable_warning), + title = stringResourceSafe(id = R.string.common_warning), + message = stringResourceSafe(id = R.string.biometric_unavailable_warning), onDismissDialog = warning.onDismiss, isDismissable = false, confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), onClick = warning.onDismiss, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt new file mode 100644 index 0000000000..8b90cbbdc9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt @@ -0,0 +1,101 @@ +package com.tangem.tap.features.welcome.ui.components + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.details.ui.cardsettings.resolveReference +import com.tangem.tap.features.welcome.component.WelcomeComponent +import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent +import com.tangem.tap.features.welcome.ui.WelcomeScreenState +import com.tangem.tap.features.welcome.ui.model.WarningModel + +@Composable +internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } + val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) + val warning by rememberUpdatedState(newValue = state.warning) + + BackHandler(onBack = state.onPopBack) + + Box( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .systemBarsPadding(), + ) { + WelcomeScreenContent( + showUnlockProgress = state.showUnlockWithBiometricsProgress, + showScanCardProgress = state.showUnlockWithCardProgress, + onUnlockClick = state.onUnlockClick, + onScanCardClick = state.onScanCardClick, + ) + + SnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = 16.dp) + .fillMaxWidth(), + hostState = snackbarHostState, + ) + } + + WarningDialog(warning) + + LaunchedEffect(errorMessage, state.onCloseError) { + errorMessage?.let { + snackbarHostState.showSnackbar(it) + state.onCloseError() + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WelcomeScreen( + @PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent, +) { + TangemThemePreview { + component.Content(Modifier) + } +} + +private class WelcomeComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewWelcomeComponent(), + PreviewWelcomeComponent( + initialState = WelcomeScreenState( + showUnlockWithBiometricsProgress = true, + showUnlockWithCardProgress = true, + ), + ), + PreviewWelcomeComponent( + initialState = WelcomeScreenState( + error = TextReference.Str(value = "Error"), + ), + ), + PreviewWelcomeComponent( + initialState = WelcomeScreenState( + warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}), + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index 7fb08b3681..c9bbbdf31c 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @@ -24,8 +24,12 @@ internal fun WelcomeScreenContent( showScanCardProgress: Boolean, onUnlockClick: () -> Unit, onScanCardClick: () -> Unit, + modifier: Modifier = Modifier, ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { SpacerHMax() Icon( modifier = Modifier.size(TangemTheme.dimens.size96), @@ -38,7 +42,7 @@ internal fun WelcomeScreenContent( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - text = stringResource(R.string.welcome_unlock_title), + text = stringResourceSafe(R.string.welcome_unlock_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -48,9 +52,9 @@ internal fun WelcomeScreenContent( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing44) .fillMaxWidth(), - text = stringResource( + text = stringResourceSafe( id = R.string.welcome_unlock_description, - stringResource(id = R.string.common_biometric_authentication), + stringResourceSafe(id = R.string.common_biometric_authentication), ), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, @@ -61,9 +65,9 @@ internal fun WelcomeScreenContent( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - text = stringResource( + text = stringResourceSafe( id = R.string.welcome_unlock, - stringResource(id = R.string.common_biometrics), + stringResourceSafe(id = R.string.common_biometrics), ), showProgress = showUnlockProgress, onClick = onUnlockClick, @@ -73,7 +77,7 @@ internal fun WelcomeScreenContent( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - text = stringResource(R.string.welcome_unlock_card), + text = stringResourceSafe(R.string.welcome_unlock_card), showProgress = showScanCardProgress, iconResId = R.drawable.ic_tangem_24, onClick = onScanCardClick, diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt new file mode 100644 index 0000000000..6685813017 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.network.auth + +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import javax.inject.Inject + +internal class DefaultVisaAuthProvider @Inject constructor( + private val authStorage: VisaAuthTokenStorage, +) : TangemVisaAuthProvider { + + override suspend fun getAuthHeader(): String { + return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 62e564e8f0..829f08d9cc 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,17 +1,21 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.tap.network.auth.* import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider +import com.tangem.tap.network.auth.DefaultVisaAuthProvider import com.tangem.utils.version.AppVersionProvider +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,7 +24,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class AuthModule { +internal class AuthModule { @Provides @Singleton @@ -51,4 +55,13 @@ class AuthModule { fun provideAppVersionProvider(): AppVersionProvider { return DefaultAppVersionProvider() } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface AuthBindModule { + + @Binds + @Singleton + fun bindVisaAuthProvider(authStorage: DefaultVisaAuthProvider): TangemVisaAuthProvider } \ 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 index 0795969b5c..54a3a0b6d6 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -13,26 +13,6 @@ class CardExchangeRules( val cardProvider: () -> CardDTO?, ) : ExchangeRules { - 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(scanResponse: ScanResponse, currency: Currency): Boolean { val card = scanResponse.card @@ -46,10 +26,6 @@ class CardExchangeRules( override fun availableForSell(currency: Currency): Boolean { val card = cardProvider() ?: return false - return when { - card.isDemoCard() -> false - card.isStart2Coin -> false - else -> true - } + return !card.isStart2Coin } } \ 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 9c0f20075c..6504558f4f 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 @@ -52,9 +52,6 @@ class CurrencyExchangeManager( _initializationStatus.value = lceContent() } - override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed() - override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed() - override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { return primaryRules.availableForBuy(scanResponse, currency) && buyService.availableForBuy(scanResponse, currency) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 7f8f63e344..f2d1e26685 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -1,5 +1,10 @@ package com.tangem.tap.network.exchangeServices +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.right import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE import com.tangem.datasource.api.express.models.response.Asset @@ -37,16 +42,6 @@ internal class DefaultRampManager( private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains) - override fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean { - return runCatching { - exchangeService?.availableForSell( - currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), - ) - } - .getOrNull() - ?: false - } - override suspend fun availableForBuy( scanResponse: ScanResponse, userWalletId: UserWalletId, @@ -66,18 +61,40 @@ internal class DefaultRampManager( ?: false } - override suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean { - return runCatching { - val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency) + override suspend fun availableForSell( + userWalletId: UserWalletId, + status: CryptoCurrencyStatus, + ): Either { + return either { + val sellSupportedByService = catch( + block = { + val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency) - if (!sellSupportedByService) return false + exchangeService?.availableForSell(currency = serviceCurrency) ?: false + }, + catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) }, + ) + + ensure(condition = sellSupportedByService) { + ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name) + } val reason = getSendUnavailabilityReason(userWalletId, status) - reason == ScenarioUnavailabilityReason.None + ensure(condition = reason is ScenarioUnavailabilityReason.None) { + when (reason) { + is ScenarioUnavailabilityReason.EmptyBalance -> { + reason.copy(withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL) + } + is ScenarioUnavailabilityReason.PendingTransaction -> { + reason.copy(withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL) + } + else -> reason + } + } + + Unit.right() } - .getOrNull() - ?: false } override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { 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 e49b386c9d..709fc68592 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 @@ -11,8 +11,6 @@ import kotlinx.coroutines.flow.StateFlow typealias ExchangeServiceInitializationStatus = Lce interface Exchanger { - fun isBuyAllowed(): Boolean - fun isSellAllowed(): Boolean fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean fun availableForSell(currency: Currency): Boolean } @@ -30,8 +28,6 @@ interface ExchangeService : Exchanger, ExchangeUrlBuilder { MutableStateFlow(value = lceLoading()) override suspend fun update() {} - override fun isBuyAllowed(): Boolean = false - override fun isSellAllowed(): Boolean = false override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false override fun getUrl( @@ -54,8 +50,6 @@ interface ExchangeRules : Exchanger { companion object { fun dummy(): ExchangeRules = object : ExchangeRules { - override fun isBuyAllowed(): Boolean = false - override fun isSellAllowed(): Boolean = false override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt index 5ee9a4ce7f..0d72c8158a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt @@ -109,6 +109,7 @@ internal val Blockchain.mercuryoNetwork: String? Blockchain.PolygonZkEVM -> null Blockchain.PolygonZkEVMTestnet -> null Blockchain.Radiant -> null + Blockchain.Fact0rn -> null Blockchain.BaseTestnet -> null Blockchain.Moonriver -> null Blockchain.MoonriverTestnet -> null @@ -140,9 +141,13 @@ internal val Blockchain.mercuryoNetwork: String? Blockchain.CoreTestnet -> null Blockchain.Chiliz -> null Blockchain.ChilizTestnet -> null + Blockchain.VanarChain -> null + Blockchain.VanarChainTestnet -> null Blockchain.Unknown -> null Blockchain.Xodex -> null Blockchain.Canxium -> null Blockchain.Clore -> null + Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null + Blockchain.Bitrock, Blockchain.BitrockTestnet -> null } } \ No newline at end of file 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 bd46579044..a892310a6f 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 @@ -54,13 +54,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E } } - override fun isBuyAllowed(): Boolean = true - - override fun isSellAllowed(): Boolean = false - override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { - if (!isBuyAllowed()) return false - val mercuryoNetwork = currency.blockchain.mercuryoNetwork val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: "" val availableCurrency = availableMercuryoCurrencies.firstOrNull { 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 90011a2d77..17cfce8e39 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 @@ -102,12 +102,6 @@ class MoonPayService( } } - override fun isBuyAllowed(): Boolean = false - - override fun isSellAllowed(): Boolean { - return status?.responseUserStatus?.isSellAllowed ?: false - } - override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean { @@ -187,6 +181,10 @@ class MoonPayService( return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) } + private fun isSellAllowed(): Boolean { + return status?.responseUserStatus?.isSellAllowed ?: false + } + companion object { const val URL_SELL = "sell.moonpay.com" } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index a4de7a4df9..729a87b31f 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -114,6 +114,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? PolygonZkEVM -> null PolygonZkEVMTestnet -> null Radiant -> null + Fact0rn -> null BaseTestnet -> null Moonriver -> null MoonriverTestnet -> null @@ -145,4 +146,8 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Xodex -> null Canxium -> null Clore -> null + VanarChain -> null + VanarChainTestnet -> null + OdysseyChain, OdysseyChainTestnet -> null + Bitrock, BitrockTestnet -> null } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index db70565310..6de664b3ce 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -4,8 +4,10 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.signer.TransactionSignerFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -35,7 +37,6 @@ import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType @@ -69,7 +70,6 @@ data class DaggerGraphState( val appRouter: AppRouter? = null, val pushNotificationsRouter: PushNotificationsRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val homeFeatureToggles: HomeFeatureToggles? = null, val getUserCountryUseCase: GetUserCountryUseCase? = null, val onrampFeatureToggles: OnrampFeatureToggles? = null, val environmentConfigStorage: EnvironmentConfigStorage? = null, @@ -77,4 +77,6 @@ data class DaggerGraphState( val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, val appPreferencesStore: AppPreferencesStore? = null, + val clipboardManager: ClipboardManager? = null, + val settingsManager: SettingsManager? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt new file mode 100644 index 0000000000..077688613f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.routing + +import android.app.Activity +import android.os.Bundle +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.routing.AppRoute +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.tap.routing.component.RoutingComponent + +@Composable +internal fun RootContent( + stack: ChildStack, + uiDependencies: UiDependencies, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + TangemTheme( + activity = context as Activity, + uiDependencies = uiDependencies, + ) { + val snackbarHostState = LocalSnackbarHostState.current + + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.primary, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = 16.dp), + hostState = snackbarHostState, + ) + }, + contentWindowInsets = WindowInsetsZero, + content = { paddingValues -> + Children( + modifier = Modifier.padding(paddingValues), + stack = stack, + ) { child -> + when (val instance = child.instance) { + is RoutingComponent.Child.Initial -> Unit + is RoutingComponent.Child.ComposableComponent -> { + instance.component.Content(Modifier.fillMaxSize()) + } + is RoutingComponent.Child.LegacyIntent -> { + // TODO: Remove and use it's own router: [REDACTED_JIRA] + startActivity(context, instance.intent, Bundle.EMPTY) + } + is RoutingComponent.Child.LegacyFragment, + -> error("Unsupported child: $instance") + } + } + }, + ) + + EventMessageEffect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt similarity index 59% rename from app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt rename to app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt index a7f5148b19..97fa1777e1 100644 --- a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt @@ -1,22 +1,28 @@ -package com.tangem.tap.routing +package com.tangem.tap.routing.component import android.content.Intent +import androidx.compose.runtime.Immutable import androidx.fragment.app.Fragment import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.value.Value import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.utils.Provider -internal interface RoutingComponent { +internal interface RoutingComponent : ComposableContentComponent { + // TODO: Remove after full navigation refactoring val router: Router + // TODO: Remove after full navigation refactoring val stack: Value> + @Immutable sealed class Child { + // TODO: Remove and find initial intent in RoutingComponent: [REDACTED_JIRA] data object Initial : Child() data class LegacyFragment( @@ -25,6 +31,10 @@ internal interface RoutingComponent { ) : Child() data class LegacyIntent(val intent: Intent) : Child() + + data class ComposableComponent( + val component: ComposableContentComponent, + ) : Child() } interface Factory { diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt new file mode 100644 index 0000000000..2f709d7943 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -0,0 +1,128 @@ +package com.tangem.tap.routing.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.observe +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.lifecycle.doOnDestroy +import com.google.android.material.snackbar.Snackbar +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.getOrCreateTyped +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.routing.RootContent +import com.tangem.tap.routing.component.RoutingComponent +import com.tangem.tap.routing.component.RoutingComponent.Child +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.tap.routing.toggle.RoutingFeatureToggles +import com.tangem.tap.routing.utils.ChildFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("LongParameterList") +internal class DefaultRoutingComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val childFactory: ChildFactory, + private val appRouterConfig: AppRouterConfig, + private val uiDependencies: UiDependencies, + routingFeatureToggles: RoutingFeatureToggles, +) : RoutingComponent, + AppComponentContext by context, + SnackbarHandler { + + private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop) + + override val stack: Value> = childStack( + source = navigationProvider.getOrCreateTyped(), + serializer = AppRoute.serializer(), + initialConfiguration = getInitialRoute(), + handleBackButton = false, + childFactory = ::child, + ) + + init { + backHandler.register(backCallback) + + lifecycle.doOnDestroy { + childFactory.doOnDestroy() + } + + if (routingFeatureToggles.isNavigationRefactoringEnabled) { + appRouterConfig.routerScope = componentScope + appRouterConfig.componentRouter = router + appRouterConfig.snackbarHandler = this + + stack.observe(lifecycle) { stack -> + val stackItems = stack.items.map { it.configuration } + + if (appRouterConfig.stack != stackItems) { + appRouterConfig.stack = stackItems + } + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stack by this.stack.subscribeAsState() + + RootContent( + modifier = modifier, + stack = stack, + uiDependencies = uiDependencies, + ) + } + + override fun showSnackbar(text: Int, length: Int, buttonTitle: Int?, action: (() -> Unit)?) { + showSnackbar( + text = resourceReference(text), + length = length, + buttonTitle = buttonTitle?.let(::resourceReference), + action = action, + ) + } + + override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) { + val message = SnackbarMessage( + message = text, + duration = when (length) { + Snackbar.LENGTH_SHORT -> SnackbarMessage.Duration.Short + Snackbar.LENGTH_LONG -> SnackbarMessage.Duration.Long + Snackbar.LENGTH_INDEFINITE -> SnackbarMessage.Duration.Indefinite + else -> SnackbarMessage.Duration.Short + }, + actionLabel = buttonTitle, + action = action, + ) + + messageSender.send(message) + } + + override fun dismissSnackbar() { + messageSender.send(SnackbarMessage(message = TextReference.EMPTY)) + } + + // TODO: Find correct initial route here: [REDACTED_JIRA] + private fun getInitialRoute(): AppRoute = AppRoute.Initial + + private fun child(route: AppRoute, context: ComponentContext): Child { + return childFactory.createChild(route) { childByContext(context) } + } + + @AssistedFactory + interface Factory : RoutingComponent.Factory { + override fun create(context: AppComponentContext): DefaultRoutingComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt deleted file mode 100644 index 77283b4c75..0000000000 --- a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.tap.routing.impl - -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.backhandler.BackCallback -import com.arkivanov.essenty.lifecycle.doOnDestroy -import com.tangem.common.routing.AppRoute -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.navigation.getOrCreateTyped -import com.tangem.tap.routing.RoutingComponent -import com.tangem.tap.routing.RoutingComponent.Child -import com.tangem.tap.routing.utils.ChildFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class DefaultRoutingComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - private val childFactory: ChildFactory, -) : RoutingComponent, AppComponentContext by context { - - private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop) - - override val stack: Value> = childStack( - source = navigationProvider.getOrCreateTyped(), - serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check - initialConfiguration = getInitialRoute(), - handleBackButton = false, - childFactory = ::child, - ) - - init { - backHandler.register(backCallback) - - lifecycle.doOnDestroy { - childFactory.doOnDestroy() - } - } - - private fun getInitialRoute(): AppRoute = AppRoute.Initial - - private fun child(route: AppRoute, context: ComponentContext): Child { - return childFactory.createChild(route) { childByContext(context) } - } - - @AssistedFactory - interface Factory : RoutingComponent.Factory { - override fun create(context: AppComponentContext): DefaultRoutingComponent - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt b/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt new file mode 100644 index 0000000000..6554684732 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.routing.toggle + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class RoutingFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isNavigationRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NAVIGATION_REFACTORING") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index e59897708c..276794edab 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -27,21 +27,22 @@ 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.WalletConnectFragment import com.tangem.tap.features.home.HomeFragment -import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment +import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.features.welcome.ui.WelcomeFragment -import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.tap.routing.component.RoutingComponent.Child +import com.tangem.tap.routing.toggle.RoutingFeatureToggles import com.tangem.utils.Provider import dagger.hilt.android.scopes.ActivityScoped import java.util.WeakHashMap import javax.inject.Inject @ActivityScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, @@ -54,6 +55,7 @@ internal class ChildFactory @Inject constructor( private val sellCryptoComponentFactory: SellCryptoComponent.Factory, private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, + private val welcomeComponentFactory: WelcomeComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -61,12 +63,173 @@ internal class ChildFactory @Inject constructor( private val stakingRouter: StakingRouter, private val testerRouter: TesterRouter, private val pushNotificationRouter: PushNotificationsRouter, + private val routingFeatureToggles: RoutingFeatureToggles, ) { - @Suppress("LongMethod", "CyclomaticComplexMethod") fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { + return if (routingFeatureToggles.isNavigationRefactoringEnabled) { + createChildNew(route, contextFactory) + } else { + createChildLegacy(route, contextFactory) + } + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun createChildNew(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { componentContexts[route] = contextFactory(route) + // region Child creation + return when (route) { + is AppRoute.Initial -> { + Child.Initial + } + is AppRoute.Details -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DetailsComponent.Params(route.userWalletId), + componentFactory = detailsComponentFactory, + ) + } + is AppRoute.Disclaimer -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DisclaimerComponent.Params(route.isTosAccepted), + componentFactory = disclaimerComponentFactory, + ) + } + is AppRoute.ManageTokens -> { + val source = when (route.source) { + AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS + AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING + AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES + } + + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = ManageTokensComponent.Params(route.userWalletId, source), + componentFactory = manageTokensComponentFactory, + ) + } + is AppRoute.Welcome -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = WelcomeComponent.Params( + intent = route.intent, + ), + componentFactory = welcomeComponentFactory, + ) + } + is AppRoute.TesterMenu -> { + Child.LegacyIntent(testerRouter.getEntryIntent()) + } + is AppRoute.WalletSettings -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = WalletSettingsComponent.Params(route.userWalletId), + componentFactory = walletSettingsComponentFactory, + ) + } + is AppRoute.MarketsTokenDetails -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = MarketsTokenDetailsComponent.Params( + token = route.token, + appCurrency = route.appCurrency, + showPortfolio = route.showPortfolio, + analyticsParams = route.analyticsParams?.let { + MarketsTokenDetailsComponent.AnalyticsParams( + blockchain = it.blockchain, + source = it.source, + ) + }, + ), + componentFactory = marketsTokenDetailsComponentFactory, + ) + } + is AppRoute.Onramp -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnrampComponent.Params( + userWalletId = route.userWalletId, + cryptoCurrency = route.currency, + source = route.source, + ), + componentFactory = onrampComponentFactory, + ) + } + is AppRoute.OnrampSuccess -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnrampSuccessComponent.Params(route.externalTxId), + componentFactory = onrampSuccessComponentFactory, + ) + } + is AppRoute.BuyCrypto -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = BuyCryptoComponent.Params(userWalletId = route.userWalletId), + componentFactory = buyCryptoComponentFactory, + ) + } + is AppRoute.SellCrypto -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SellCryptoComponent.Params(userWalletId = route.userWalletId), + componentFactory = sellCryptoComponentFactory, + ) + } + is AppRoute.SwapCrypto -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId), + componentFactory = swapSelectTokensComponentFactory, + ) + } + is AppRoute.Onboarding -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnboardingEntryComponent.Params( + scanResponse = route.scanResponse, + startBackupFlow = route.startFromBackup, + multiWalletMode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding + AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + }, + ), + componentFactory = onboardingEntryComponentFactory, + ) + } + is AppRoute.AccessCodeRecovery, + is AppRoute.AppCurrencySelector, + is AppRoute.SaveWallet, + is AppRoute.Send, + is AppRoute.AppSettings, + is AppRoute.CardSettings, + is AppRoute.DetailsSecurity, + is AppRoute.Home, + is AppRoute.OnboardingNote, + is AppRoute.OnboardingOther, + is AppRoute.OnboardingTwins, + is AppRoute.OnboardingWallet, + is AppRoute.QrScanning, + is AppRoute.ReferralProgram, + is AppRoute.ResetToFactory, + is AppRoute.Swap, + is AppRoute.Wallet, + is AppRoute.WalletConnectSessions, + is AppRoute.CurrencyDetails, + is AppRoute.Staking, + is AppRoute.PushNotification, + -> error("Unsupported route: $route") + } + // endregion + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun createChildLegacy(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { + componentContexts[route] = contextFactory(route) + + // region Child creation return when (route) { is AppRoute.Initial -> { Child.Initial @@ -77,9 +240,6 @@ internal class ChildFactory @Inject constructor( is AppRoute.AppCurrencySelector -> { route.asFragmentChild(Provider { AppCurrencySelectorFragment() }) } - is AppRoute.ModalNotification -> { - route.asFragmentChild(Provider { ModalNotificationBottomSheetFragment() }) - } is AppRoute.SaveWallet -> { route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) } @@ -248,6 +408,7 @@ internal class ChildFactory @Inject constructor( ) } } + // endregion } fun doOnDestroy() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt index bdade79e4e..e7aa49f4d9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -7,7 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.tap.DecomposeFragment -import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.utils.Provider internal fun AppRoute.asFragmentChild(fragmentProvider: Provider): Child { @@ -37,4 +37,14 @@ internal fun > createComponentChild( + contextProvider: Provider, + params: P, + componentFactory: F, +): Child { + val context = contextProvider() + + return Child.ComposableComponent(componentFactory.create(context, params)) } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index aa962825e1..dac7964d66 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,4 +6,10 @@ android:background="@color/background_primary" android:filterTouchesWhenObscured="true"> + + \ No newline at end of file diff --git a/ci_resources/build_ci_gradle.properties b/ci_resources/build_ci_gradle.properties new file mode 100644 index 0000000000..bb0256c8c4 --- /dev/null +++ b/ci_resources/build_ci_gradle.properties @@ -0,0 +1,13 @@ +org.gradle.jvmargs=-Xmx7g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.workers.max=8 +org.gradle.daemon=false +org.gradle.unsafe.configuration-cache=false +org.gradle.console=plain +org.gradle.caching=false + +android.useAndroidX=true +android.nonTransitiveRClass=false +android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2 + +kotlin.incremental=true diff --git a/ci_resources/tests_8gb_ram_ci_gradle.properties b/ci_resources/tests_8gb_ram_ci_gradle.properties new file mode 100644 index 0000000000..78577a0975 --- /dev/null +++ b/ci_resources/tests_8gb_ram_ci_gradle.properties @@ -0,0 +1,13 @@ +org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=512m -XX:+UseParallelGC -XX:ParallelGCThreads=3 -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.workers.max=3 +org.gradle.daemon=false +org.gradle.unsafe.configuration-cache=false +org.gradle.console=plain +org.gradle.caching=false + +android.useAndroidX=true +android.nonTransitiveRClass=false +android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2 + +kotlin.incremental=false diff --git a/ci_resources/x86_64_libs/ld-linux-x86-64.so.2 b/ci_resources/x86_64_libs/ld-linux-x86-64.so.2 new file mode 100755 index 0000000000..8ef4df1a3d Binary files /dev/null and b/ci_resources/x86_64_libs/ld-linux-x86-64.so.2 differ diff --git a/ci_resources/x86_64_libs/libc.so.6 b/ci_resources/x86_64_libs/libc.so.6 new file mode 100755 index 0000000000..4b600449a0 Binary files /dev/null and b/ci_resources/x86_64_libs/libc.so.6 differ diff --git a/ci_resources/x86_64_libs/libdl.so.2 b/ci_resources/x86_64_libs/libdl.so.2 new file mode 100644 index 0000000000..2862f8ef7e Binary files /dev/null and b/ci_resources/x86_64_libs/libdl.so.2 differ diff --git a/ci_resources/x86_64_libs/libgcc_s.so.1 b/ci_resources/x86_64_libs/libgcc_s.so.1 new file mode 100644 index 0000000000..0e7b8a5939 Binary files /dev/null and b/ci_resources/x86_64_libs/libgcc_s.so.1 differ diff --git a/ci_resources/x86_64_libs/libm.so.6 b/ci_resources/x86_64_libs/libm.so.6 new file mode 100644 index 0000000000..7a473b15f3 Binary files /dev/null and b/ci_resources/x86_64_libs/libm.so.6 differ diff --git a/ci_resources/x86_64_libs/libpthread.so.0 b/ci_resources/x86_64_libs/libpthread.so.0 new file mode 100644 index 0000000000..3e09ebef74 Binary files /dev/null and b/ci_resources/x86_64_libs/libpthread.so.0 differ diff --git a/ci_resources/x86_64_libs/librt.so.1 b/ci_resources/x86_64_libs/librt.so.1 new file mode 100644 index 0000000000..8edf9c4312 Binary files /dev/null and b/ci_resources/x86_64_libs/librt.so.1 differ diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 6707a2bbb2..055efc6a2a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,7 +1,35 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } + +android { + namespace = "com.tangem.common" +} + dependencies { + implementation(projects.core.utils) + + // region Firebase libraries + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.firebase.crashlytics) + implementation(deps.firebase.messaging) + // end + + implementation(deps.timber) + + implementation(deps.arrow.core) + + implementation(deps.test.junit) + implementation(deps.test.truth) + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // end } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bc9dae984e..c81997cf54 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -252,9 +252,6 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") - @Serializable - data object ModalNotification : AppRoute(path = "/modal_notification") - @Serializable data class Staking( val userWalletId: UserWalletId, @@ -326,11 +323,16 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}") - // Onboarding V2 + /** + * Onboarding V2 + * @property scanResponse scan response, determines onboarding route by the product type + * @property startFromBackup (MultiWallet param, doesn't affect other types) start onboarding from backup + * @property mode (MultiWallet param, doesn't affect other types) onboarding mode + */ @Serializable data class Onboarding( val scanResponse: ScanResponse, - val startFromBackup: Boolean, + val startFromBackup: Boolean = false, val mode: Mode = Mode.Onboarding, ) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}") { diff --git a/common/src/main/java/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt similarity index 100% rename from common/src/main/java/com/tangem/common/TangemBlogUrlBuilder.kt rename to common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt diff --git a/common/src/main/kotlin/com/tangem/common/keyboard/KeyboardValidator.kt b/common/src/main/kotlin/com/tangem/common/keyboard/KeyboardValidator.kt new file mode 100644 index 0000000000..76850cee8a --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/keyboard/KeyboardValidator.kt @@ -0,0 +1,83 @@ +package com.tangem.common.keyboard + +import android.content.Context +import android.provider.Settings +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Keyboard validator + * + * @property context application context + */ +@Singleton +class KeyboardValidator @Inject constructor( + @ApplicationContext private val context: Context, +) { + + /** Get keyboard identifier [KeyboardID] */ + fun getKeyboardId(): KeyboardID? { + val id = Settings.Secure.getString( + this.context.contentResolver, + Settings.Secure.DEFAULT_INPUT_METHOD, + ) ?: return null + + return KeyboardID(value = id) + } + + /** Check if [id] is trusted */ + fun validate(id: KeyboardID): Boolean = trustedIdentifiers.contains(id.getPackageName()) + + /** + * Keyboard identifier + * + * @property value value + */ + @JvmInline + value class KeyboardID(val value: String) { + + /** Get package name */ + fun getPackageName(): String? = value.split("/").firstOrNull() + } + + private companion object { + + val trustedIdentifiers = listOf( + // Google + "com.android.inputmethod.latin", + "com.google.android.inputmethod.latin", + "com.google.android.tts", + + // Samsung + "com.sec.android.inputmethod.latin", + "com.sec.android.inputmethod/.SamsungVoiceIME", + "com.samsung.android.honeyboard", + + // Microsoft + "com.microsoft.SwiftKeyApp", + "com.touchtype.swiftkey", + "com.touchtype.swiftkey.beta", + + // HtC + "com.htc.sense.ime.langpack.tger", + + // LG + "com.lge.ime", + + // Huawei + "com.huawei.ohos.inputmethod", + + // Third party + "com.menny.android.anysoftkeyboard", + "ch.icoaching.wrio", + "rkr.simplekeyboard.inputmethod", + "keepass2android.keepass2android", + "keepass2android.keepass2android_nonet", + "com.softwarevalencia.openboard.inputmethod.latin", + "kl.ime.oh", + "com.syntellia.fleksy.keyboard", + "org.pocketworkstation.pckeyboard", + ) + } +} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt b/common/src/main/kotlin/com/tangem/common/module/ModuleErrorCode.kt similarity index 100% rename from common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt rename to common/src/main/kotlin/com/tangem/common/module/ModuleErrorCode.kt diff --git a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt b/common/src/main/kotlin/com/tangem/common/module/ModuleMessage.kt similarity index 100% rename from common/src/main/java/com/tangem/common/module/ModuleMessage.kt rename to common/src/main/kotlin/com/tangem/common/module/ModuleMessage.kt diff --git a/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt new file mode 100644 index 0000000000..9f3973c29b --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt @@ -0,0 +1,31 @@ +package com.tangem.common.uri + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import timber.log.Timber +import java.net.URI + +/** + * External url validator + * +[REDACTED_AUTHOR] + */ +object ExternalUrlValidator { + + private val trustedHost: List = listOf("tangem.com") + + /** Check if [externalUri] is trusted */ + fun isUriTrusted(externalUri: String): Boolean { + return try { + val uri = URI.create(externalUri) + + uri.scheme == "https" && uri.host in trustedHost + } catch (e: Exception) { + val exception = IllegalStateException("Failed to validate URI: $externalUri", e) + + Timber.e(exception) + FirebaseCrashlytics.getInstance().recordException(exception) + + false + } + } +} \ No newline at end of file diff --git a/common/src/testDebug/kotlin/com/tangem/common/uri/ExternalUrlValidatorTest.kt b/common/src/testDebug/kotlin/com/tangem/common/uri/ExternalUrlValidatorTest.kt new file mode 100644 index 0000000000..f03d64dde6 --- /dev/null +++ b/common/src/testDebug/kotlin/com/tangem/common/uri/ExternalUrlValidatorTest.kt @@ -0,0 +1,36 @@ +package com.tangem.common.uri + +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +class ExternalUrlValidatorTest(private val model: Model) { + + @Test + fun test() { + val actual = ExternalUrlValidator.isUriTrusted(externalUri = model.url) + + Truth.assertThat(actual).isEqualTo(model.expected) + } + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection = listOf( + Model(url = "https://tangem.com", expected = true), + Model(url = "https://tange.com", expected = false), + Model(url = "https://fake.tangem.com", expected = false), + Model(url = "http://tangem.com", expected = false), + Model(url = "http://tandem.com", expected = false), + Model(url = "adawdawdassdw", expected = false), + ) + + data class Model(val url: String, val expected: Boolean) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index b279e47f6a..f1d8e921a8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig @@ -21,6 +20,7 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.fiaticon.FiatIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.PersistentList @@ -62,7 +62,7 @@ internal fun LazyListScope.buttons( SpacerWMax() } Text( - text = stringResource(R.string.send_max_amount), + text = stringResourceSafe(R.string.send_max_amount), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, modifier = Modifier diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt index 8689591d9b..930c31ce42 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt @@ -7,27 +7,24 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.R import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.common.ui.R import com.tangem.core.ui.res.TangemThemePreview @Composable fun SendDoneButtons( txUrl: String, onExploreClick: () -> Unit, - onShareClick: () -> Unit, + onShareClick: (String) -> Unit, isVisible: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current AnimatedVisibility( visible = isVisible && txUrl.isNotBlank(), @@ -38,19 +35,18 @@ fun SendDoneButtons( ) { Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) { SecondaryButtonIconStart( - text = stringResource(id = R.string.common_explore), + text = stringResourceSafe(id = R.string.common_explore), iconResId = R.drawable.ic_web_24, onClick = onExploreClick, modifier = Modifier.weight(1f), ) SpacerW12() SecondaryButtonIconStart( - text = stringResource(id = R.string.common_share), + text = stringResourceSafe(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - onShareClick() + onShareClick(txUrl) }, modifier = Modifier.weight(1f), ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 4f4e74d559..4291b55a7f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -54,7 +53,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { if (isPermissionAlertShow) { BasicDialog( message = content.data.dialogText.resolveReference(), - title = stringResource(id = R.string.common_approve), + title = stringResourceSafe(id = R.string.common_approve), confirmButton = DialogButtonUM { isPermissionAlertShow = false }, onDismissDialog = {}, ) @@ -87,7 +86,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh SpacerH(height = TangemTheme.dimens.spacing20) PrimaryButtonIconEnd( - text = stringResource(id = R.string.common_approve), + text = stringResourceSafe(id = R.string.common_approve), iconResId = R.drawable.ic_tangem_24, showProgress = data.approveButton.loading, modifier = Modifier @@ -100,7 +99,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh SpacerH12() SecondaryButton( - text = stringResource(id = R.string.common_cancel), + text = stringResourceSafe(id = R.string.common_cancel), modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), @@ -180,7 +179,7 @@ private fun AmountItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringResource(id = R.string.give_permission_rows_amount, currency), + text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, maxLines = 1, @@ -259,10 +258,10 @@ private fun DropdownSelector( Row { Text( text = when (item) { - ApproveType.LIMITED -> stringResource( + ApproveType.LIMITED -> stringResourceSafe( id = R.string.give_permission_current_transaction, ) - ApproveType.UNLIMITED -> stringResource(id = R.string.give_permission_unlimited) + ApproveType.UNLIMITED -> stringResourceSafe(id = R.string.give_permission_unlimited) }, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, @@ -297,7 +296,7 @@ private fun Preview_GiveTxPermissionBottomSheet() { TangemThemePreview { GiveTxPermissionBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = previewData, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt index 85522dd450..2703481292 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt @@ -7,12 +7,12 @@ 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.res.stringResource import com.tangem.common.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowApprox import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @Suppress("LongParameterList") @@ -47,7 +47,7 @@ fun ExpressEstimate( ), ) { Text( - text = stringResource(id = R.string.express_estimated_amount), + text = stringResourceSafe(id = R.string.express_estimated_amount), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt index c83e14ae0a..1f8f4f8eb3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -25,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -50,7 +50,7 @@ fun ExpressProvider( .fillMaxWidth(), ) { Text( - text = stringResource(id = R.string.express_provider), + text = stringResourceSafe(id = R.string.express_provider), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -62,7 +62,9 @@ fun ExpressProvider( .clickable { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(providerTxId)) - Toast.makeText(context, R.string.express_transaction_id_copied, Toast.LENGTH_SHORT).show() + Toast + .makeText(context, R.string.express_transaction_id_copied, Toast.LENGTH_SHORT) + .show() }, ) { Icon( @@ -76,7 +78,7 @@ fun ExpressProvider( ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = stringResource(R.string.express_transaction_id, providerTxId), + text = stringResourceSafe(R.string.express_transaction_id, providerTxId), maxLines = 1, overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.body2, diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt index ce513e4e50..35905de8f7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt @@ -6,7 +6,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.R @@ -15,6 +14,7 @@ import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @Composable @@ -22,14 +22,14 @@ fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) { Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( - text = stringResource(id = R.string.common_transaction_status), + text = stringResourceSafe(id = R.string.common_transaction_status), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier.align(CenterHorizontally), ) SpacerH10() Text( - text = stringResource(id = R.string.express_exchange_status_subtitle), + text = stringResourceSafe(id = R.string.express_exchange_status_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index cb7a3743ec..c534f12308 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -19,9 +19,9 @@ sealed class NavigationButtonsState { data class NavigationButton( val textReference: TextReference, @DrawableRes val iconRes: Int? = null, - val isSecondary: Boolean, - val isIconVisible: Boolean, - val showProgress: Boolean, - val isEnabled: Boolean, + val isSecondary: Boolean = false, + val isIconVisible: Boolean = false, + val showProgress: Boolean = false, + val isEnabled: Boolean = true, val onClick: () -> Unit, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt index 09c66ed8c8..a2eed383f5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt @@ -6,23 +6,27 @@ import com.tangem.core.ui.extensions.resourceReference object ExpressNotificationsUM { - data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning( + data class NeedVerification(val onGoToProviderClick: (() -> Unit)?) : NotificationUM.Warning( title = resourceReference(R.string.express_exchange_notification_verification_title), subtitle = resourceReference(R.string.express_exchange_notification_verification_text), iconResId = R.drawable.ic_alert_triangle_20, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_go_to_provider), - onClick = onGoToProviderClick, - ), + buttonsState = onGoToProviderClick?.let { onClick -> + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_provider), + onClick = onClick, + ) + }, ) - data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error( + data class FailedByProvider(val onGoToProviderClick: (() -> Unit)?) : NotificationUM.Error( title = resourceReference(R.string.express_exchange_notification_failed_title), subtitle = resourceReference(R.string.express_exchange_notification_failed_text), iconResId = R.drawable.ic_alert_circle_24, - buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_go_to_provider), - onClick = onGoToProviderClick, - ), + buttonState = onGoToProviderClick?.let { onClick -> + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_provider), + onClick = onClick, + ) + }, ) } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 9f5bf56e7d..cb008ba04a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -245,6 +245,16 @@ sealed class NotificationUM(val config: NotificationConfig) { title = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_title), subtitle = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_message), ) + + data object SellingRegionalRestriction : Warning( + title = resourceReference(id = R.string.selling_regional_restriction_alert_title), + subtitle = resourceReference(id = R.string.selling_regional_restriction_alert_message), + ) + + data object InsufficientBalanceForSelling : Warning( + title = resourceReference(id = R.string.selling_insufficient_balance_alert_title), + subtitle = resourceReference(id = R.string.selling_insufficient_balance_alert_message), + ) } open class Info( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index adf9f08d85..2f18a10314 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -1,7 +1,6 @@ package com.tangem.common.ui.notifications import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString @@ -149,10 +148,19 @@ object NotificationsFactory { } } + /** + * Adds Existential Warning + * + * @param existentialDeposit existential deposit of blockchain + * @param feeAmount amount of fee spending for transaction + * @param sendingAmount amount sending by user (excluding fee for coins) + * @param cryptoCurrencyStatus blockchain currency status + * @param onReduceClick action to leave existential amount in balance after transaction + */ fun MutableList.addExistentialWarningNotification( existentialDeposit: BigDecimal?, feeAmount: BigDecimal, - receivedAmount: BigDecimal, + sendingAmount: BigDecimal, cryptoCurrencyStatus: CryptoCurrencyStatus, onReduceClick: ( reduceAmountBy: BigDecimal, @@ -165,7 +173,7 @@ object NotificationsFactory { val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { feeAmount } else { - receivedAmount + sendingAmount } val diff = balance.minus(spendingAmount) if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { @@ -283,9 +291,9 @@ object NotificationsFactory { fun MutableList.addValidateTransactionNotifications( dustValue: BigDecimal, - fee: Fee?, validationError: Throwable?, cryptoCurrency: CryptoCurrency, + minAdaValue: BigDecimal?, // TODO revert to Fee, after swap TxFee refactored onReduceClick: ( reduceAmountTo: BigDecimal, notification: Class, @@ -301,11 +309,11 @@ object NotificationsFactory { error = validationError, onReduceClick = onReduceClick, ) - null -> (fee as? Fee.CardanoToken)?.let { + null -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( tokenName = cryptoCurrency.name, - minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + minAdaValue = minAdaValue.parseBigDecimal(cryptoCurrency.decimals), ), ) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt index 7d7460a633..3e71bc838b 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt @@ -23,4 +23,16 @@ sealed class TechAnalyticsEvent( FULLY, } } + + class KeyboardIdentifier(id: String, packageName: String?, isTrusted: Boolean) : TechAnalyticsEvent( + event = "Keyboard Identifier", + params = buildMap { + put("Id", id) + packageName?.let { + put("Package", it) + put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName") + } + put("isTrusted", isTrusted.toString()) + }, + ) } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json index ae77ec7b7c..75cedce0e4 100644 --- a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json +++ b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json @@ -10,5 +10,21 @@ { "name": "NEXA/test", "version": "undefined" + }, + { + "name": "fact0rn", + "version": "undefined" + }, + { + "name": "vanar-chain", + "version": "undefined" + }, + { + "name": "bitrock", + "version": "undefined" + }, + { + "name": "odyssey", + "version": "undefined" } ] \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index f7af0b38fc..6592384f45 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -11,10 +11,6 @@ "name": "IS_ETHEREUM_EIP_1559_ENABLED", "version": "5.17.0" }, - { - "name": "MIGRATE_USER_COUNTRY_CODE_ENABLED", - "version": "5.17.0" - }, { "name": "ONRAMP_ENABLED", "version": "undefined" @@ -26,5 +22,17 @@ { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", "version": "undefined" + }, + { + "name": "NAVIGATION_REFACTORING", + "version": "undefined" + }, + { + "name": "SWAP_STORIES_ENABLED", + "version": "undefined" + }, + { + "name": "VISA_ONBOARDING_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index e47f1f8494..1b2a53ec3c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -2,7 +2,8 @@ package com.tangem.datasource.api.common import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend +import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor @@ -44,11 +45,13 @@ internal class SwitchEnvironmentInterceptor( .build() } - private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { - headers.forEach { (name, valueProvider) -> - val value = valueProvider() + private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { + runBlocking { + headers.forEach { (name, valueProvider) -> + val value = valueProvider() - if (value.isNotBlank()) addHeader(name = name, value = value) + if (value.isNotBlank()) addHeader(name = name, value = value) + } } return this diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 65bc7a1e66..100cc04382 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -24,6 +24,8 @@ sealed class ApiConfig { Express, TangemTech, StakeKit, + TangemVisaAuth, + TangemVisa, } private fun initializeId(): ID { @@ -31,6 +33,8 @@ sealed class ApiConfig { is Express -> ID.Express is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit + is TangemVisaAuth -> ID.TangemVisaAuth + is TangemVisa -> ID.TangemVisa } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt index 4ab1342caf..5c6bc0fc7e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt @@ -1,6 +1,6 @@ package com.tangem.datasource.api.common.config -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend /** * Api environment config @@ -14,5 +14,5 @@ import com.tangem.utils.Provider data class ApiEnvironmentConfig( val environment: ApiEnvironment, val baseUrl: String, - val headers: Map> = emptyMap(), + val headers: Map> = emptyMap(), ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 2be7c447f4..a0c28a6880 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.utils.RequestHeader import com.tangem.lib.auth.ExpressAuthProvider -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider /** @@ -47,11 +47,11 @@ internal class Express( ) private fun createHeaders(isProd: Boolean) = buildMap { - put(key = "api-key", value = Provider { getApiKey(isProd) }) - put(key = "user-id", value = Provider(expressAuthProvider::getUserId)) - put(key = "session-id", value = Provider(expressAuthProvider::getSessionId)) + put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) }) + put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId)) + put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId)) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values) - put(key = "refcode", value = Provider(expressAuthProvider::getRefCode)) + put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode)) } private fun getApiKey(isProd: Boolean): String { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt index 4370c06c92..0efb5068d1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.lib.auth.StakeKitAuthProvider -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend /** * StakeKit [ApiConfig] @@ -25,8 +25,8 @@ internal class StakeKit( environment = ApiEnvironment.PROD, baseUrl = "https://api.stakek.it/v1/", headers = mapOf( - "X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey), - "accept" to Provider { "application/json" }, + "X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey), + "accept" to ProviderSuspend { "application/json" }, ), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt new file mode 100644 index 0000000000..2f7ea8a41f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider +import com.tangem.utils.ProviderSuspend + +internal class TangemVisa( + private val authProvider: TangemVisaAuthProvider, +) : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs = listOf( + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://bff.tangem.com/", + headers = createHeaders(), + ) + + private fun createHeaders() = mapOf( + "Authorization" to ProviderSuspend { authProvider.getAuthHeader() }, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt new file mode 100644 index 0000000000..5a26524206 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.utils.ProviderSuspend + +internal class TangemVisaAuth : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE + + override val environmentConfigs = listOf( + createStageEnvironment(), + ) + + private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.STAGE, + baseUrl = "https://api-s.tangem.org/", + headers = createHeaders(), + ) + + private fun createHeaders() = mapOf>() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt new file mode 100644 index 0000000000..62197282df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.api.common.visa + +interface TangemVisaAuthProvider { + + suspend fun getAuthHeader(): String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt deleted file mode 100644 index 4c08c5ce73..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.datasource.api.tangemTech - -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.datasource.utils.RequestHeader -import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader -import com.tangem.datasource.utils.RequestHeader.CacheControlHeader -import com.tangem.datasource.utils.addHeaders -import com.tangem.datasource.utils.addLoggers -import okhttp3.OkHttpClient -import retrofit2.Retrofit - -/** -[REDACTED_AUTHOR] - */ -// TODO("Remove after removing Redux") -@Deprecated("Use TangemTechApi") -object TangemTechService { - - var api: TangemTechApi = createApi() - private set - - private const val TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" - - fun addAuthenticationHeader(header: AuthenticationHeader) { - api = createApi(header) - } - - private fun createApi(header: RequestHeader? = null): TangemTechApi { - val headers = mutableListOf(CacheControlHeader).apply { header?.let(::add) } - return Retrofit.Builder() - .addConverterFactory(MoshiConverter.networkMoshiConverter) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(TANGEM_TECH_BASE_URL) - .client( - OkHttpClient.Builder() - .addHeaders(*headers.toTypedArray()) - .addLoggers() - .build(), - ) - .build() - .create(TangemTechApi::class.java) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt new file mode 100644 index 0000000000..afdded620a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa + +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import retrofit2.http.GET + +interface TangemVisaApi { + + @GET("activation-status") + suspend fun getRemoteActivationStatus(): CardActivationRemoteStateResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt new file mode 100644 index 0000000000..858390cf7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt @@ -0,0 +1,47 @@ +package com.tangem.datasource.api.visa + +import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse +import com.tangem.datasource.api.visa.models.response.JWTResponse +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.Headers +import retrofit2.http.POST + +interface TangemVisaAuthApi { + + @FormUrlEncoded + @Headers("Content-Type: application/x-www-form-urlencoded") + @POST("auth/clients/mobile-app-android/nonce-challenge") + suspend fun generateNonceByWalletAddress( + @Field("customer_id") customerId: String? = null, + @Field("customer_wallet_address") customerWalletAddress: String, + ): GenerateNonceResponse + + @FormUrlEncoded + @Headers("Content-Type: application/x-www-form-urlencoded") + @POST("auth/clients/mobile-app-android/nonce-challenge") + suspend fun generateNonceByCard( + @Field("card_id") cardId: String, + @Field("card_public_key") cardPublicKey: String, + ): GenerateNonceResponse + + @FormUrlEncoded + @Headers("Content-Type: application/x-www-form-urlencoded") + @POST("auth/protocol/openid-connect/token") + suspend fun getAccessToken( + @Field("client_id") clientId: String = "mobile-app-android", + @Field("grant_type") grantType: String = "password", + @Field("session_id") sessionId: String, + @Field("signature") signature: String, + @Field("salt") salt: String?, + ): JWTResponse + + @FormUrlEncoded + @Headers("Content-Type: application/x-www-form-urlencoded") + @POST("auth/protocol/openid-connect/token") + suspend fun refreshAccessToken( + @Field("client_id") clientId: String = "mobile-app-android", + @Field("grant_type") grantType: String = "refresh_token", + @Field("refresh_token") refreshToken: String, + ): JWTResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt new file mode 100644 index 0000000000..1b4c053a91 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.visa.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CardActivationRemoteStateResponse( + @Json(name = "state") val state: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt new file mode 100644 index 0000000000..002e43b16e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNonceResponse( + @Json(name = "nonce") val nonce: String, + @Json(name = "session_id") val sessionId: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt new file mode 100644 index 0000000000..e4fc0d2579 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.visa.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class JWTResponse( + @Json(name = "access_token") val accessToken: String, + @Json(name = "expires_in") val expiresIn: Int, + @Json(name = "refresh_expires_in") val refreshExpiresIn: Int, + @Json(name = "refresh_token") val refreshToken: String, + @Json(name = "token_type") val tokenType: String, + @Json(name = "not-before-policy") val notBeforePolicy: Int, + @Json(name = "session_state") val sessionState: String, + @Json(name = "scope") val scope: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index c5f506627e..a9576c3042 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -1,10 +1,11 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.StakeKit import com.tangem.datasource.api.common.config.TangemTech +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -39,4 +40,13 @@ internal object ApiConfigsModule { @IntoSet fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig = TangemTech(appVersionProvider, authProvider) + + @Provides + @IntoSet + fun provideTangemAuthVisaConfig(): ApiConfig = TangemVisaAuth() + + @Provides + @IntoSet + fun provideTangemVisaConfig(tangemVisaAuthProvider: TangemVisaAuthProvider): ApiConfig = + TangemVisa(tangemVisaAuthProvider) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 0a3b4fef73..0294c9d76a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.LocalDateAdapter import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.domain.models.scan.serialization.* +import com.tangem.domain.visa.model.VisaCardActivationStatus import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,6 +35,7 @@ class MoshiModule { .add(BigDecimalAdapter()) .add(LocalDateAdapter()) .add(DateTimeAdapter()) + .add(VisaCardActivationStatus.jsonAdapter) .add(KotlinJsonAdapterFactory()) .addStakeKitEnumFallbackAdapters() .build() @@ -42,8 +44,26 @@ class MoshiModule { @Provides @Singleton @SdkMoshi - fun provideSdkMoshi(sdkMoshiJsonConverter: MoshiJsonConverter): Moshi { - return sdkMoshiJsonConverter.moshi + fun provideSdkMoshi(): Moshi { + val adapters = MoshiJsonConverter.getTangemSdkAdapters() + + listOf( + BigDecimalAdapter(), + WalletDerivedKeysMapAdapter(), + ScanResponseDerivedKeysMapAdapter(), + ByteArrayKeyAdapter(), + ExtendedPublicKeysMapAdapter(), + CardBackupStatusAdapter(), + DerivationPathAdapterWithMigration(), + ) + + val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters() + + return Moshi.Builder().apply { + add(VisaCardActivationStatus.jsonAdapter) + adapters.forEach { this.add(it) } + typedAdapters.forEach { add(it.key, it.value) } + add(KotlinJsonAdapterFactory()) + }.build() } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 8b38ca983a..ae75ab68d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -15,6 +15,8 @@ import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 +import com.tangem.datasource.api.visa.TangemVisaApi +import com.tangem.datasource.api.visa.TangemVisaAuthApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* @@ -169,6 +171,48 @@ internal object NetworkModule { ) } + @Provides + @Singleton + fun provideTangemVisaAuthApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, + ): TangemVisaAuthApi { + return createApi( + id = ApiConfig.ID.TangemVisaAuth, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, + ) + } + + @Provides + @Singleton + fun provideTangemVisaApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, + ): TangemVisaApi { + return createApi( + id = ApiConfig.ID.TangemVisa, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, + ) + } + @Deprecated("use createApi instead") private inline fun provideTangemTechApiInternal( moshi: Moshi, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt index 3ed335349d..7d82c0cac3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt @@ -87,6 +87,19 @@ internal object BlockchainSDKConfigConverter : Converter>): OkHttpClient.Builder { +internal fun OkHttpClient.Builder.addHeaders( + requestHeaders: Map>, +): OkHttpClient.Builder { return addInterceptor( Interceptor { chain -> val request = chain.request().newBuilder().apply { - requestHeaders.forEach { - val value = it.value.invoke() - - if (value.isNotBlank()) addHeader(name = it.key, value = value) + runBlocking { + requestHeaders.forEach { + val value = it.value.invoke() + if (value.isNotBlank()) addHeader(name = it.key, value = value) + } } }.build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index 5b9de76ce7..a3ae68e61b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.utils import android.os.Build import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.utils.RequestHeader.CacheControlHeader.checkHeaderValueOrEmpty -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider import java.util.Locale import java.util.TimeZone @@ -13,26 +13,26 @@ import java.util.TimeZone * * @param pairs header name and header value pairs */ -sealed class RequestHeader(vararg pairs: Pair>) { +sealed class RequestHeader(vararg pairs: Pair>) { /** Header list */ - val values: Map> = pairs.toMap() + val values: Map> = pairs.toMap() - data object CacheControlHeader : RequestHeader("Cache-Control" to Provider { "max-age=600" }) + data object CacheControlHeader : RequestHeader("Cache-Control" to ProviderSuspend { "max-age=600" }) class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader( - "card_id" to Provider(authProvider::getCardId), - "card_public_key" to Provider(authProvider::getCardPublicKey), + "card_id" to ProviderSuspend(authProvider::getCardId), + "card_public_key" to ProviderSuspend(authProvider::getCardPublicKey), ) class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader( - "version" to Provider(appVersionProvider::versionName), - "platform" to Provider { "android" }, - "language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() }, - "timezone" to Provider { + "version" to ProviderSuspend { appVersionProvider.versionName }, + "platform" to ProviderSuspend { "android" }, + "language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() }, + "timezone" to ProviderSuspend { TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty() }, - "device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, + "device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, ) /** diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 7c874508d8..5d92d7abd8 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -10,12 +10,15 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider -import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,12 +31,15 @@ private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() private val appAuthProvider = mockk() +private val visaAuthProvider = mockk() // Don't forget to add new config !!! private val API_CONFIGS = setOf( Express(configManager, expressAuthProvider, appVersionProvider), TangemTech(appVersionProvider, appAuthProvider), StakeKit(stakeKitAuthProvider), + TangemVisaAuth(), + TangemVisa(visaAuthProvider), ) /** @@ -53,6 +59,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY every { appAuthProvider.getCardId() } returns APP_CARD_ID every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY + coEvery { visaAuthProvider.getAuthHeader() } returns VISA_AUTH_HEADER } @Test @@ -62,8 +69,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { Truth.assertThat(actual.environment).isEqualTo(model.expected.environment) Truth.assertThat(actual.baseUrl).isEqualTo(model.expected.baseUrl) - Truth.assertThat(actual.headers.mapValues { it.value() }) - .isEqualTo(model.expected.headers.mapValues { it.value() }) + Truth.assertThat(actual.headers.mapValues { runBlocking { it.value() } }) + .isEqualTo(model.expected.headers.mapValues { runBlocking { it.value() } }) } data class Model(val id: ApiConfig.ID, val expected: ApiEnvironmentConfig) @@ -77,6 +84,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { const val STAKE_KIT_API_KEY = "stake_kit_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "app_public_key" + const val VISA_AUTH_HEADER = "Bearer visa_auth_header" @JvmStatic @Parameterized.Parameters @@ -85,6 +93,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is Express -> createExpressModel() is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() + is TangemVisaAuth -> createVisaAuthModel() + is TangemVisa -> createVisaModel() } } @@ -115,23 +125,23 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") }, headers = mapOf( - "api-key" to Provider { + "api-key" to ProviderSuspend { if (environment == ApiEnvironment.PROD) { MockEnvironmentConfigStorage.EXPRESS_API_KEY } else { MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY } }, - "user-id" to Provider { EXPRESS_USER_ID }, - "session-id" to Provider { EXPRESS_SESSION_ID }, - "refcode" to Provider { EXPRESS_REF_CODE }, - "version" to Provider { VERSION_NAME }, - "platform" to Provider { "android" }, - "language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() }, - "timezone" to Provider { + "user-id" to ProviderSuspend { EXPRESS_USER_ID }, + "session-id" to ProviderSuspend { EXPRESS_SESSION_ID }, + "refcode" to ProviderSuspend { EXPRESS_REF_CODE }, + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "android" }, + "language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() }, + "timezone" to ProviderSuspend { TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty() }, - "device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, + "device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, ), ), ) @@ -144,15 +154,15 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { environment = ApiEnvironment.PROD, baseUrl = "https://api.tangem-tech.com/v1/", headers = mapOf( - "card_id" to Provider { APP_CARD_ID }, - "card_public_key" to Provider { APP_CARD_PUBLIC_KEY }, - "version" to Provider { VERSION_NAME }, - "platform" to Provider { "android" }, - "language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() }, - "timezone" to Provider { + "card_id" to ProviderSuspend { APP_CARD_ID }, + "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "android" }, + "language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() }, + "timezone" to ProviderSuspend { TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty() }, - "device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, + "device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() }, ), ), ) @@ -165,8 +175,31 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { environment = ApiEnvironment.PROD, baseUrl = "https://api.stakek.it/v1/", headers = mapOf( - "X-API-KEY" to Provider { STAKE_KIT_API_KEY }, - "accept" to Provider { "application/json" }, + "X-API-KEY" to ProviderSuspend { STAKE_KIT_API_KEY }, + "accept" to ProviderSuspend { "application/json" }, + ), + ), + ) + } + + private fun createVisaAuthModel(): Model { + return Model( + id = ApiConfig.ID.TangemVisaAuth, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.STAGE, + baseUrl = "https://api-s.tangem.org/", + ), + ) + } + + private fun createVisaModel(): Model { + return Model( + id = ApiConfig.ID.TangemVisa, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://bff.tangem.com/", + headers = mapOf( + "Authorization" to ProviderSuspend { VISA_AUTH_HEADER }, ), ), ) diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt index 6ffebce9fe..d4453d3e6a 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt @@ -7,8 +7,6 @@ import com.tangem.core.decompose.navigation.AppNavigationProvider import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider import com.tangem.core.decompose.navigation.DefaultRouter import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.DefaultUiMessageSender -import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.utils.ComponentCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -16,9 +14,9 @@ import kotlinx.coroutines.CoroutineScope class DefaultAppComponentContext( componentContext: ComponentContext, - messageHandler: UiMessageHandler, override val dispatchers: CoroutineDispatcherProvider, override val hiltComponentBuilder: DecomposeComponent.Builder, + override val messageSender: UiMessageSender, private val replaceRouter: Router? = null, ) : AppComponentContext, ComponentContext by componentContext { @@ -26,8 +24,6 @@ class DefaultAppComponentContext( override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers) - override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler) - override val navigationProvider: AppNavigationProvider get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() } diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt index b9e3987632..f6bbc51b20 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt @@ -3,4 +3,7 @@ package com.tangem.core.decompose.di import javax.inject.Qualifier @Qualifier -annotation class RootAppComponentContext \ No newline at end of file +annotation class RootAppComponentContext + +@Qualifier +annotation class GlobalUiMessageSender \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt index 59546d42e0..52da85b3c7 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt @@ -1,6 +1,6 @@ package com.tangem.core.decompose.ui -internal class DefaultUiMessageSender( +class DefaultUiMessageSender( private val handler: UiMessageHandler, ) : UiMessageSender { diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt index 25c4706fb9..a5148c68c0 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt @@ -1,5 +1,6 @@ package com.tangem.core.navigation.settings class DummySettingsManager : SettingsManager { - override fun openSettings() { /* no-op */ } + override fun openAppSettings() = Unit + override fun openBiometricSettings() = Unit } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt index 4be904c9cb..20ffee2885 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt @@ -1,5 +1,8 @@ package com.tangem.core.navigation.settings interface SettingsManager { - fun openSettings() + + fun openAppSettings() + + fun openBiometricSettings() } \ No newline at end of file diff --git a/core/res/build.gradle.kts b/core/res/build.gradle.kts index c2fce27feb..c444a12b01 100644 --- a/core/res/build.gradle.kts +++ b/core/res/build.gradle.kts @@ -6,4 +6,17 @@ plugins { android { namespace = "com.tangem.core.res" +} + +dependencies { + + implementation(projects.core.utils) + + implementation(deps.timber) + + // region Firebase libraries + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.firebase.crashlytics) + // endregion } \ No newline at end of file diff --git a/core/res/src/main/java/com/tangem/core/res/Resources.kt b/core/res/src/main/java/com/tangem/core/res/Resources.kt new file mode 100644 index 0000000000..46ceaa8b3a --- /dev/null +++ b/core/res/src/main/java/com/tangem/core/res/Resources.kt @@ -0,0 +1,82 @@ +package com.tangem.core.res + +import android.content.res.Resources +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.utils.SupportedLanguages +import timber.log.Timber + +/** + * Get a string resource safely or the resource name if an exception is thrown + * + * @param id resource id + */ +fun Resources.getStringSafe(@StringRes id: Int): String { + return runCatching { getString(id) } + .getOrResourceName(resources = this, id) +} + +/** + * Get a string resource safely or the resource name if an exception is thrown + * + * @param id resource id + * @param formatArgs format args + */ +fun Resources.getStringSafe(@StringRes id: Int, vararg formatArgs: Any): String { + return runCatching { getString(id, *formatArgs) } + .recoverCatching { + // If something goes wrong, returns the resource without arguments + val string = getString(id) + + reportIssue(resources = this, id, *formatArgs) + + string + } + .getOrResourceName(resources = this, id, *formatArgs) +} + +/** + * Get a plural string resource safely or the resource name if an exception is thrown + * + * @param id resource id + * @param count count + */ +fun Resources.getPluralStringSafe(@PluralsRes id: Int, count: Int): String { + return runCatching { getQuantityString(id, count) } + .getOrResourceName(resources = this, id) +} + +/** + * Get a plural string resource safely or the resource name if an exception is thrown + * + * @param id resource id + * @param count count + * @param formatArgs format args + */ +fun Resources.getPluralStringSafe(@PluralsRes id: Int, count: Int, vararg formatArgs: Any): String { + return runCatching { getQuantityString(id, count, *formatArgs) } + .getOrResourceName(resources = this, id, *formatArgs) +} + +private fun Result.getOrResourceName(resources: Resources, id: Int, vararg formatArgs: Any): String { + return getOrElse { + reportIssue(resources, id, formatArgs) + + // If something still goes wrong, returns the resource name + resources.getResourceEntryName(id) + } +} + +private fun reportIssue(resources: Resources, id: Int, vararg formatArgs: Any) { + val exception = IllegalStateException( + "An error occurred while parsing the string:\n" + + "\tname: R.string.${resources.getResourceEntryName(id)}\n" + + "\targs: ${formatArgs.joinToString(prefix = "[", postfix = "]") { it.toString() }}\n" + + "\tlocale: ${SupportedLanguages.getCurrentSupportedLanguageCode()}\n", + ) + + Timber.tag("Resources").e(exception) + + FirebaseCrashlytics.getInstance().recordException(exception) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt index 2e6354a2e2..cfac79d5c4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt @@ -9,6 +9,19 @@ import androidx.compose.runtime.Stable @Stable interface ClipboardManager { - /** Copies to clipboard [text] with optional [label] */ - fun setText(label: String = "", text: String) + /** + * Copies to clipboard [text] with optional [label] + * + * @param text text + * @param isSensitive flag that determines whether the text is sensitive + * @param label label + */ + fun setText(text: String, isSensitive: Boolean, label: String = "") + + /** + * Get text (text/plain) from clipboard + * + * @param default default value + */ + fun getText(default: String? = null): String? } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index e43a390155..be5351a233 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.ButtonColors +import androidx.compose.material3.ButtonColors import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index e741b087fe..0eab14b8e9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -8,15 +8,14 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.RadioButton -import androidx.compose.material.RadioButtonDefaults -import androidx.compose.material.Text +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -27,6 +26,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleDialogTextField +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList @@ -318,14 +318,14 @@ private fun DialogButtons( ) { if (dismissButton != null) { DialogButton( - text = dismissButton.title ?: stringResource(id = R.string.common_cancel), + text = dismissButton.title ?: stringResourceSafe(id = R.string.common_cancel), warning = dismissButton.warning, enabled = dismissButton.enabled, onClick = dismissButton.onClick, ) } DialogButton( - text = confirmButton.title ?: stringResource(id = R.string.common_ok), + text = confirmButton.title ?: stringResourceSafe(id = R.string.common_ok), warning = confirmButton.warning, enabled = confirmButton.enabled, onClick = confirmButton.onClick, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 22399d6e4b..c433f996ea 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -2,7 +2,6 @@ package com.tangem.core.ui.components.bottomsheets import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* import androidx.compose.material3.* @@ -99,7 +98,7 @@ inline fun DefaultBottomSheet( crossinline title: @Composable (BoxScope.(T) -> Unit), crossinline content: @Composable (ColumnScope.(T) -> Unit), ) { - var isVisible by remember { mutableStateOf(value = config.isShow) } + var isVisible by remember { mutableStateOf(value = config.isShown) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) if (isVisible && config.content is T) { @@ -114,8 +113,8 @@ inline fun DefaultBottomSheet( ) } - LaunchedEffect(key1 = config.isShow) { - if (config.isShow) { + LaunchedEffect(key1 = config.isShown) { + if (config.isShown) { isVisible = true } else { sheetState.collapse { isVisible = false } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt index d2eb1f9935..002cecf115 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt @@ -3,12 +3,12 @@ package com.tangem.core.ui.components.bottomsheets /** * Tangem bottom sheet config * - * @property isShow flag that determine if bottom sheet is shown + * @property isShown flag that determine if bottom sheet is shown * @property onDismissRequest lambda be invoked when bottom sheet is dismissed * @property content content config */ data class TangemBottomSheetConfig( - val isShow: Boolean, + val isShown: Boolean, val onDismissRequest: () -> Unit, val content: TangemBottomSheetConfigContent, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt new file mode 100644 index 0000000000..f0f478d567 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt @@ -0,0 +1,156 @@ +package com.tangem.core.ui.components.bottomsheets.message + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.isNullOrEmpty +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun MessageBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { notification: MessageBottomSheetUM -> + Content(model = notification) + } +} + +@Composable +internal fun Content(model: MessageBottomSheetUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(40.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(modifier = Modifier) + + if (model.iconResId != null) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(model.iconResId), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (!model.title.isNullOrEmpty()) { + Text( + text = model.title.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + } + + Text( + text = model.message.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (model.primaryAction != null) { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = model.primaryAction.text.resolveReference(), + enabled = model.primaryAction.enabled, + onClick = model.primaryAction.onClick, + ) + } + + if (model.secondaryAction != null) { + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = model.secondaryAction.text.resolveReference(), + enabled = model.secondaryAction.enabled, + onClick = model.secondaryAction.onClick, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_NotificationBottomSheet( + @PreviewParameter(MessageBottomSheetUMPreviewProvider::class) params: MessageBottomSheetUM, +) { + TangemThemePreview { + MessageBottomSheet( + config = TangemBottomSheetConfig( + content = params, + isShown = true, + onDismissRequest = {}, + ), + ) + } +} + +private class MessageBottomSheetUMPreviewProvider : PreviewParameterProvider { + val balancesHiddenMessage = MessageBottomSheetUM( + iconResId = R.drawable.ic_eye_off_outline_24, + title = resourceReference(R.string.balance_hidden_title), + message = resourceReference(R.string.balance_hidden_description), + primaryAction = MessageBottomSheetUM.ActionUM( + text = resourceReference(R.string.balance_hidden_got_it_button), + onClick = {}, + ), + secondaryAction = MessageBottomSheetUM.ActionUM( + text = resourceReference(R.string.balance_hidden_do_not_show_button), + onClick = {}, + ), + ) + + override val values: Sequence + get() = sequenceOf( + balancesHiddenMessage, + balancesHiddenMessage.copy(title = null), + balancesHiddenMessage.copy(iconResId = null), + balancesHiddenMessage.copy(primaryAction = null), + balancesHiddenMessage.copy(secondaryAction = null), + balancesHiddenMessage.copy( + primaryAction = null, + secondaryAction = null, + ), + balancesHiddenMessage.copy( + title = null, + iconResId = null, + primaryAction = null, + secondaryAction = null, + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUM.kt new file mode 100644 index 0000000000..88a40bd846 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetUM.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.components.bottomsheets.message + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +data class MessageBottomSheetUM( + @DrawableRes val iconResId: Int?, + val title: TextReference?, + val message: TextReference, + val primaryAction: ActionUM?, + val secondaryAction: ActionUM?, +) : TangemBottomSheetConfigContent { + + data class ActionUM( + val text: TextReference, + val enabled: Boolean = true, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 0e39a22357..d6f38a3567 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -20,16 +20,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import com.tangem.core.res.getStringSafe import com.tangem.core.ui.R import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet @@ -75,10 +73,9 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi ) Buttons( - address = selectedAddress.value, snackbarHostState = snackbarHostState, - onShareClick = content.onShareClick, - onCopyClick = content.onCopyClick, + onShareClick = { content.onShareClick(selectedAddress.value) }, + onCopyClick = { content.onCopyClick(selectedAddress.value) }, ) } } @@ -96,7 +93,7 @@ private fun Info(currency: String, network: String, showMemoDisclaimer: Boolean, modifier = Modifier .padding(horizontal = 18.dp) .fillMaxWidth(), - text = stringResource(R.string.receive_bottom_sheet_no_memo_required_message), + text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -191,7 +188,7 @@ private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Pa verticalArrangement = Arrangement.spacedBy(24.dp), ) { Text( - text = stringResource( + text = stringResourceSafe( R.string.receive_bottom_sheet_warning_message, getName(content = content, index = currentIndex), content.symbol, @@ -229,14 +226,12 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String @Composable private fun Buttons( - address: String, snackbarHostState: SnackbarHostState, onShareClick: () -> Unit, onCopyClick: () -> Unit, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val clipboardManager = LocalClipboardManager.current val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val resources = context.resources @@ -247,17 +242,16 @@ private fun Buttons( ) { SecondaryButtonIconStart( modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_copy), + text = stringResourceSafe(id = R.string.common_copy), iconResId = R.drawable.ic_copy_24, onClick = { onCopyClick() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clipboardManager.setText(AnnotatedString(address)) coroutineScope.launch { snackbarHostState.showSnackbar( - message = resources.getString(R.string.wallet_notification_address_copied), + message = resources.getStringSafe(R.string.wallet_notification_address_copied), ) } }, @@ -265,13 +259,12 @@ private fun Buttons( SecondaryButtonIconStart( modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_share), + text = stringResourceSafe(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { - onShareClick() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(address) + + onShareClick() }, ) } @@ -286,7 +279,7 @@ private fun Preview_TokenReceiveBottomSheet( ) { TangemThemePreview { val config = TangemBottomSheetConfig( - isShow = true, + isShown = true, content = params, onDismissRequest = {}, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt index 29d19f5300..eee9fdb3bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -9,6 +9,6 @@ data class TokenReceiveBottomSheetConfig( val network: String, val addresses: ImmutableList, val showMemoDisclaimer: Boolean, - val onCopyClick: () -> Unit, - val onShareClick: () -> Unit, + val onCopyClick: (String) -> Unit, + val onShareClick: (String) -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 0dad99ad18..efacaad36c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -6,7 +6,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt index f4235c7619..be1de590a2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt @@ -1,25 +1,17 @@ package com.tangem.core.ui.components.buttons.common -import androidx.compose.material.ButtonColors +import androidx.compose.material3.ButtonColors import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.graphics.Color -data class TangemButtonColors( - private val backgroundColor: Color, - private val contentColor: Color, - private val disabledBackgroundColor: Color, - private val disabledContentColor: Color, -) : ButtonColors { +@Composable +fun ButtonColors.containerColor(enabled: Boolean): State { + return rememberUpdatedState(newValue = if (enabled) containerColor else disabledContainerColor) +} - @Composable - override fun backgroundColor(enabled: Boolean): State { - return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor) - } - - @Composable - override fun contentColor(enabled: Boolean): State { - return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor) - } +@Composable +fun ButtonColors.contentColor(enabled: Boolean): State { + return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt index 5a555dfcf7..615757fd50 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt @@ -1,8 +1,8 @@ package com.tangem.core.ui.components.buttons.common -import androidx.compose.material.ButtonColors -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.ButtonElevation +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color @@ -12,7 +12,7 @@ object TangemButtonsDefaults { val elevation: ButtonElevation @Composable - get() = ButtonDefaults.elevation( + get() = ButtonDefaults.buttonElevation( defaultElevation = TangemTheme.dimens.elevation0, pressedElevation = TangemTheme.dimens.elevation0, ) @@ -20,60 +20,60 @@ object TangemButtonsDefaults { val primaryButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = TangemTheme.colors.button.primary, + get() = ButtonColors( + containerColor = TangemTheme.colors.button.primary, contentColor = TangemTheme.colors.text.primary2, - disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContainerColor = TangemTheme.colors.button.disabled, disabledContentColor = TangemTheme.colors.text.disabled, ) val secondaryButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = TangemTheme.colors.button.secondary, + get() = ButtonColors( + containerColor = TangemTheme.colors.button.secondary, contentColor = TangemTheme.colors.text.primary1, - disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContainerColor = TangemTheme.colors.button.disabled, disabledContentColor = TangemTheme.colors.text.disabled, ) val defaultTextButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = Color.Transparent, + get() = ButtonColors( + containerColor = Color.Transparent, contentColor = TangemTheme.colors.text.secondary, - disabledBackgroundColor = Color.Transparent, + disabledContainerColor = Color.Transparent, disabledContentColor = TangemTheme.colors.text.disabled, ) val warningTextButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = Color.Transparent, + get() = ButtonColors( + containerColor = Color.Transparent, contentColor = TangemTheme.colors.text.warning, - disabledBackgroundColor = Color.Transparent, + disabledContainerColor = Color.Transparent, disabledContentColor = TangemTheme.colors.text.disabled, ) val selectorButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = Color.Transparent, + get() = ButtonColors( + containerColor = Color.Transparent, contentColor = TangemTheme.colors.text.tertiary, - disabledBackgroundColor = Color.Transparent, + disabledContainerColor = Color.Transparent, disabledContentColor = TangemTheme.colors.text.disabled, ) val positiveButtonColors: ButtonColors @Composable @ReadOnlyComposable - get() = TangemButtonColors( - backgroundColor = Color.Transparent, + get() = ButtonColors( + containerColor = Color.Transparent, contentColor = TangemTheme.colors.text.accent, - disabledBackgroundColor = Color.Transparent, + disabledContainerColor = Color.Transparent, disabledContentColor = TangemTheme.colors.text.disabled, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt new file mode 100644 index 0000000000..bf8c61037a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt @@ -0,0 +1,38 @@ +package com.tangem.core.ui.components.fields.contextmenu + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.NonSkippableComposable +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.TextToolbar +import androidx.compose.ui.platform.TextToolbarStatus + +/** + * Composable component that used to disable context menu + * + * @param content content + */ +@Composable +@NonSkippableComposable +fun DisableContextMenu(content: @Composable () -> Unit) { + CompositionLocalProvider( + value = LocalTextToolbar provides EmptyTextToolbar, + content = content, + ) +} + +private object EmptyTextToolbar : TextToolbar { + + override val status: TextToolbarStatus = TextToolbarStatus.Hidden + + override fun hide() = Unit + + override fun showMenu( + rect: Rect, + onCopyRequested: (() -> Unit)?, + onPasteRequested: (() -> Unit)?, + onCutRequested: (() -> Unit)?, + onSelectAllRequested: (() -> Unit)?, + ) = Unit +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index ef0308abd3..df498f267a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -4,7 +4,10 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -14,7 +17,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -24,8 +26,9 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev) @@ -112,7 +115,7 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: ) if (showTag) { Text( - text = stringResource(R.string.express_provider_best_rate), + text = stringResourceSafe(R.string.express_provider_best_rate), style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.accent, modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index 79c75c96e6..ef23a4babc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -18,9 +18,9 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION @@ -51,7 +51,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi modifier = modifier, ) { Text( - text = stringResource(R.string.common_paste), + text = stringResourceSafe(R.string.common_paste), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.primary2, modifier = Modifier @@ -86,7 +86,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) { Icon( painter = painterResource(id = R.drawable.ic_close_24), tint = TangemTheme.colors.icon.informative, - contentDescription = stringResource(R.string.common_close), + contentDescription = stringResourceSafe(R.string.common_close), modifier = modifier .size(TangemTheme.dimens.size24) .clickable( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 8ef37d1f7c..52bdac7eab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -18,6 +17,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -61,7 +61,7 @@ private fun Title(currencyName: String, modifier: Modifier = Modifier) { contentAlignment = Alignment.CenterStart, ) { Text( - text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName), + text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, currencyName), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) @@ -157,7 +157,7 @@ private fun LoadingContent() { @Composable private fun QuoteTimeStatus() { Text( - text = stringResource(id = R.string.wallet_marketprice_block_update_time), + text = stringResourceSafe(id = R.string.wallet_marketprice_block_update_time), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt index 6b5f396f85..8bb1793279 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt @@ -3,7 +3,8 @@ package com.tangem.core.ui.components.notifications import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Text +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,7 +16,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -71,10 +71,10 @@ fun NoteMigrationNotification(config: NotificationConfig, modifier: Modifier = M text = button.text.resolveReference(), icon = TangemButtonIconPosition.None, onClick = button.onClick, - colors = TangemButtonColors( - backgroundColor = Light2, + colors = ButtonColors( + containerColor = Light2, contentColor = Dark6, - disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContainerColor = TangemTheme.colors.button.disabled, disabledContentColor = TangemTheme.colors.text.disabled, ), enabled = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index d76569543f..40e33e05ac 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.material3.ButtonColors import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -24,7 +25,6 @@ import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -146,10 +146,10 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = text = button?.text?.resolveReference().orEmpty(), icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24), onClick = button?.onClick ?: {}, - colors = TangemButtonColors( - backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, + colors = ButtonColors( + containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, contentColor = Dark6, - disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContainerColor = TangemTheme.colors.button.disabled, disabledContentColor = TangemTheme.colors.text.disabled, ), textStyle = TangemTheme.typography.subtitle1, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt index 94f39ed5dd..beadc7ce9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.material3.ButtonColors import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -22,7 +23,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -120,10 +120,10 @@ private fun Button(config: NotificationConfig) { text = button.text.resolveReference(), icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24), onClick = button.onClick, - colors = TangemButtonColors( - backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, + colors = ButtonColors( + containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, contentColor = Dark6, - disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContainerColor = TangemTheme.colors.button.disabled, disabledContentColor = TangemTheme.colors.text.disabled, ), textStyle = TangemTheme.typography.subtitle1, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt index 41d6020243..05cbf5db85 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt @@ -2,7 +2,9 @@ package com.tangem.core.ui.components.progressbar +import android.content.res.Configuration import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable @@ -15,10 +17,12 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.progressBarRangeInfo import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlin.math.abs /** @@ -123,4 +127,18 @@ private fun DrawScope.drawLinearIndicator( } private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) = - drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap) \ No newline at end of file + drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap) + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LinearProgressIndicator_Preview() { + TangemThemePreview { + LinearProgressIndicator( + progress = { 0.5f }, + modifier = Modifier.fillMaxWidth(), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index c6dd8c39f9..86009a8f7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -23,8 +22,9 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns @Composable @@ -77,7 +77,7 @@ fun SelectorRowItem( contentDescription = null, ) Text( - text = stringResource(titleRes), + text = stringResourceSafe(titleRes), style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt index d1cd229f6d..9f9fdabff9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt @@ -9,15 +9,18 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemThemePreview /** * Tangem snackbar host. - * Based on Material3 component. It's best way to show [TangemSnackbar]. + * Based on Material3 component. It's the best way to show [TangemSnackbar]. * - * @param hostState snackbar host state - * @param modifier modifier - * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * @param modifier Modifier to be applied to the layout. + * @param hostState SnackbarHostState that will be used to show snackbar. + * Optional parameter. By default, it uses [LocalSnackbarHostState.current]. + * @param actionOnNewLine Flag that indicates if the action should be displayed on a new line. + * Optional parameter, `false` by default. * * @see Figma @@ -25,7 +28,11 @@ import com.tangem.core.ui.res.TangemThemePreview [REDACTED_AUTHOR] */ @Composable -fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { +fun TangemSnackbarHost( + modifier: Modifier = Modifier, + hostState: SnackbarHostState = LocalSnackbarHostState.current, + actionOnNewLine: Boolean = false, +) { SnackbarHost(hostState = hostState, modifier = modifier) { data -> TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt new file mode 100644 index 0000000000..0bcf8ace95 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -0,0 +1,111 @@ +package com.tangem.core.ui.components.stories + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.stories.inner.StoriesClickableArea +import com.tangem.core.ui.components.stories.inner.StoriesProgressBar +import com.tangem.core.ui.components.stories.inner.StoriesStepStateMachine +import com.tangem.core.ui.components.stories.model.StoriesContentConfig +import com.tangem.core.ui.components.stories.model.StoryConfig +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Stories container component. + * Implements base logic for switching story pages and handling clicks + * + * @param config stories configuration + * @param modifier composable modifier + * @param isPauseStories pause stories progression externally + * @param currentStoryContent content of current displaying story + */ +@Composable +inline fun StoriesContainer( + config: StoriesContentConfig, + modifier: Modifier = Modifier, + isPauseStories: Boolean = false, + crossinline currentStoryContent: @Composable BoxScope.(T, Boolean) -> Unit, +) { + var isPressed by remember { mutableStateOf(value = false) } + val storyState by remember(config) { + mutableStateOf( + StoriesStepStateMachine( + stories = config.stories, + isRepeatable = config.isRestartable, + ), + ) + } + + val isPaused = isPressed || isPauseStories + + Box(modifier = modifier) { + StoriesClickableArea( + onPress = { isPressed = it }, + onPreviousStory = storyState::prevStory, + onNextStory = storyState::nextStory, + ) + + currentStoryContent(storyState.currentStory, isPaused) + + StoriesProgressBar( + steps = storyState.steps, + currentStep = storyState.currentIndex.intValue, + stepDuration = storyState.currentStory.duration, + paused = isPaused, + onStepFinish = { + if (storyState.nextStory()) { + config.onClose() + } + }, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StoriesContainer_Preview() { + TangemThemePreview { + StoriesContainer( + config = StoryContainerPreviewProviderData(), + modifier = Modifier.background(Color(0xFF010101)), + isPauseStories = false, + currentStoryContent = { pageConfig, _ -> + Text( + text = pageConfig.title, + color = TangemColorPalette.White, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center), + ) + }, + ) + } +} + +private data class StoryContainerPreviewProviderData( + override val stories: ImmutableList = persistentListOf( + StoryConfigPreviewProviderData(title = "Tangem"), + StoryConfigPreviewProviderData(title = "Cold"), + StoryConfigPreviewProviderData(title = "Wallet"), + ), + override val isRestartable: Boolean = true, + override val onClose: () -> Unit = {}, +) : StoriesContentConfig + +private data class StoryConfigPreviewProviderData( + override val duration: Int = 3_000, + val title: String, +) : StoryConfig +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesClickableArea.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesClickableArea.kt new file mode 100644 index 0000000000..e5d18fd061 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesClickableArea.kt @@ -0,0 +1,56 @@ +package com.tangem.core.ui.components.stories.inner + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput + +private const val STORY_HOLD_DELAY = 200 + +@Composable +fun StoriesClickableArea(onPress: (Boolean) -> Unit, onPreviousStory: () -> Unit, onNextStory: () -> Unit) { + Row( + modifier = Modifier.fillMaxSize(), + ) { + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + onPress(true) + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < STORY_HOLD_DELAY) onPreviousStory() + onPress(false) + }, + ) + }, + ) + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + onPress(true) + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < STORY_HOLD_DELAY) onNextStory() + onPress(false) + }, + ) + }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt new file mode 100644 index 0000000000..5f009d8569 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt @@ -0,0 +1,128 @@ +package com.tangem.core.ui.components.stories.inner + +import android.provider.Settings +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.delay + +private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L +const val STORY_DURATION = 8_000 + +private suspend fun animationProgress( + paused: Boolean, + animatorSpeed: Float, + stepDuration: Int, + progress: Animatable, + onStepFinish: () -> Unit, +) { + if (paused) { + progress.stop() + } else { + if (animatorSpeed == 0f) { + progress.snapTo(1f) + delay(STORIES_ANIMATION_SPEED_ZERO_DURATION) + } else { + progress.animateTo( + targetValue = 1f, + animationSpec = tween( + durationMillis = (stepDuration * (1f - progress.value)).toInt(), + easing = LinearEasing, + ), + ) + progress.snapTo(0f) + } + onStepFinish() + } +} + +@Composable +fun StoriesProgressBar( + steps: Int, + currentStep: Int, + paused: Boolean = false, + stepDuration: Int = STORY_DURATION, + onStepFinish: () -> Unit = {}, +) { + val progress = remember(currentStep) { Animatable(initialValue = 0f) } + + val context = LocalContext.current + val animatorSpeed = Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) + + LaunchedEffect(paused, currentStep, animatorSpeed) { + animationProgress( + paused = paused, + animatorSpeed = animatorSpeed, + stepDuration = stepDuration, + progress = progress, + onStepFinish = onStepFinish, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .statusBarsPadding() + .padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + ), + ) { + for (index in 0..steps) { + Row( + modifier = Modifier + .height(2.dp) + .weight(1f) + .clip(RoundedCornerShape(2.dp)) + .background(TangemColorPalette.White.copy(alpha = .2f)), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(2.dp)) + .background(TangemColorPalette.White) + .fillMaxHeight() + .let { + when (index) { + currentStep -> it.fillMaxWidth(progress.value) + in 0..currentStep -> it.fillMaxWidth(fraction = 1f) + else -> it + } + }, + ) + } + SpacerW4() + } + } +} + +@Preview +@Composable +private fun StoriesProgressBarPreview() { + Box( + modifier = Modifier + .wrapContentSize() + .background(TangemColorPalette.Black) + .padding(vertical = TangemTheme.dimens.spacing16), + ) { + StoriesProgressBar(steps = 5, currentStep = 3, paused = false) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesStepStateMachine.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesStepStateMachine.kt new file mode 100644 index 0000000000..37c26dd443 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesStepStateMachine.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.stories.inner + +import androidx.compose.runtime.IntState +import androidx.compose.runtime.asIntState +import androidx.compose.runtime.mutableIntStateOf +import kotlinx.collections.immutable.ImmutableList + +class StoriesStepStateMachine( + private val stories: ImmutableList, + private val isRepeatable: Boolean, +) { + + private var _currentIndex = mutableIntStateOf(FIRST_INDEX) + + val steps = stories.lastIndex + val currentIndex: IntState + get() = _currentIndex.asIntState() + + val currentStory + get() = stories[currentIndex.intValue.coerceIn(FIRST_INDEX, steps)] + + fun nextStory(): Boolean { + _currentIndex.intValue = if (currentIndex.intValue == steps && isRepeatable) { + FIRST_INDEX + } else { + currentIndex.intValue.inc().coerceAtMost(stories.size) + } + + return currentIndex.intValue > steps + } + + fun prevStory() { + _currentIndex.intValue = currentIndex.intValue.dec().coerceAtLeast(FIRST_INDEX) + } + + private companion object { + const val FIRST_INDEX = 0 + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt new file mode 100644 index 0000000000..f5e569292a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.components.stories.model + +import kotlinx.collections.immutable.ImmutableList + +/** + * Config for stories component + * + * @property stories configuration list + * @property isRestartable indicates than stories progressions starts + */ +interface StoriesContentConfig { + val stories: ImmutableList + val isRestartable: Boolean + val onClose: () -> Unit +} + +interface StoryConfig { + val duration: Int +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/NonFiatContentBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/NonFiatContentBlock.kt index 5e486a5078..7682b67444 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/NonFiatContentBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/NonFiatContentBlock.kt @@ -10,10 +10,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder @@ -67,7 +67,7 @@ private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?) @Composable private fun NonFiatContentText(@StringRes text: Int) { Text( - text = stringResource(id = text), + text = stringResourceSafe(id = text), color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt index 77720d8cd9..0ea91f89b4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt @@ -6,13 +6,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.audits.AuditLabel import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State as TokenCryptoAmountState @@ -33,7 +33,7 @@ internal fun TokenCryptoAmount( AuditLabel(state = state.auditLabelUM, modifier = modifier) } is TokenCryptoAmountState.Unreachable -> { - CryptoAmountText(amount = stringResource(id = R.string.common_unreachable), modifier = modifier) + CryptoAmountText(amount = stringResourceSafe(id = R.string.common_unreachable), modifier = modifier) } is TokenCryptoAmountState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 1a4d6d8dae..34bd4345fa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem -import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -25,9 +24,6 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { when (state) { - is TokensListItemUM.NetworkGroupTitle -> { - NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier) - } is TokensListItemUM.GroupTitle -> { GroupTitleItem(state, modifier) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt index c79e40c723..a3f882b7ba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt @@ -1,12 +1,32 @@ package com.tangem.core.ui.components.tokenlist.internal +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder +import org.burnoutcrew.reorderable.rememberReorderableLazyListState /** * Group title item @@ -18,14 +38,90 @@ import com.tangem.core.ui.res.TangemTheme */ @Composable fun GroupTitleItem(state: TokensListItemUM.GroupTitle, modifier: Modifier = Modifier) { + BaseGroupTitleItem(text = state.text, modifier = modifier) +} + +/** + * Draggable network group item + * + * @param state state + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + */ +@Composable +fun DraggableGroupTitleItem( + state: TokensListItemUM.GroupTitle, + reorderableTokenListState: ReorderableLazyListState, + modifier: Modifier = Modifier, +) { + BaseGroupTitleItem( + text = state.text, + modifier = modifier, + action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) }, + ) +} + +@Composable +private fun BaseGroupTitleItem( + text: TextReference, + modifier: Modifier = Modifier, + action: (@Composable BoxScope.() -> Unit)? = null, +) { NetworkTitle( title = { Text( - text = state.text.resolveReference(), + text = text.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) }, modifier = modifier, + action = action, ) -} \ No newline at end of file +} + +@Composable +private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) { + Box( + modifier = Modifier.detectReorder(reorderableTokenListState), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NetworkTitleItemPreview(@PreviewParameter(GroupTitleItemProvider::class) isDraggable: Boolean) { + TangemThemePreview { + val state = TokensListItemUM.GroupTitle( + id = 1, + text = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(resourceReference(id = R.string.main_tokens)), + ), + ) + + if (isDraggable) { + DraggableGroupTitleItem( + state = state, + reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }), + modifier = Modifier.background(color = TangemTheme.colors.background.primary), + ) + } else { + GroupTitleItem( + state = state, + modifier = Modifier.background(color = TangemTheme.colors.background.primary), + ) + } + } +} + +private object GroupTitleItemProvider : CollectionPreviewParameterProvider(collection = listOf(true, false)) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt deleted file mode 100644 index 8ca65a2982..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.core.ui.components.tokenlist.internal - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.R -import com.tangem.core.ui.components.rows.NetworkTitle -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import org.burnoutcrew.reorderable.ReorderableLazyListState -import org.burnoutcrew.reorderable.detectReorder -import org.burnoutcrew.reorderable.rememberReorderableLazyListState - -/** - * Network title item - * - * @param networkName network name - * @param modifier modifier - */ -@Composable -internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) { - BaseNetworkTitleItem(networkName = networkName, modifier = modifier) -} - -/** - * Draggable network title item - * - * @param networkName network name - * @param reorderableTokenListState reorderable token list state - * @param modifier modifier - */ -@Composable -fun DraggableNetworkTitleItem( - networkName: String, - reorderableTokenListState: ReorderableLazyListState, - modifier: Modifier = Modifier, -) { - BaseNetworkTitleItem( - networkName = networkName, - modifier = modifier, - action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) }, - ) -} - -@Composable -private fun BaseNetworkTitleItem( - networkName: String, - modifier: Modifier = Modifier, - action: (@Composable BoxScope.() -> Unit)? = null, -) { - NetworkTitle( - title = { - Text( - text = stringResource(id = R.string.wallet_network_group_title, networkName), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - modifier = modifier, - action = action, - ) -} - -@Composable -private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) { - Box( - modifier = Modifier.detectReorder(reorderableTokenListState), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = rememberVectorPainter( - image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24), - ), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun NetworkTitleItemPreview(@PreviewParameter(NetworkTitleItemProvider::class) isDraggable: Boolean) { - TangemThemePreview { - if (isDraggable) { - DraggableNetworkTitleItem( - networkName = "Ethereum", - reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }), - modifier = Modifier.background(color = TangemTheme.colors.background.primary), - ) - } else { - NetworkTitleItem( - networkName = "Ethereum", - modifier = Modifier.background(color = TangemTheme.colors.background.primary), - ) - } - } -} - -private object NetworkTitleItemProvider : CollectionPreviewParameterProvider(collection = listOf(true, false)) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 23332310aa..9b0e9235ed 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -23,15 +23,6 @@ sealed interface TokensListItemUM { val searchBarUM: SearchBarUM, ) : TokensListItemUM - /** - * Network group title - * - * @property id id - * @property name network group name - */ - @Deprecated("Use GroupTitle instead") // TODO: [REDACTED_JIRA] - data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM - /** * Group title * diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index af0ff0f89b..8c7bdbc6d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -234,7 +233,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Content -> { Text( text = when (state.status) { - is Status.Failed -> stringResource(id = R.string.common_transaction_failed) + is Status.Failed -> stringResourceSafe(id = R.string.common_transaction_failed) else -> state.subtitle.resolveReference() }, modifier = modifier, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt index 3a7f89969c..4f8201f07c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt @@ -10,11 +10,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Transactions block title @@ -35,7 +35,7 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = stringResource(id = R.string.common_transactions), + text = stringResourceSafe(id = R.string.common_transactions), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) @@ -52,7 +52,7 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod contentDescription = null, ) Text( - text = stringResource(id = R.string.common_explorer), + text = stringResourceSafe(id = R.string.common_explorer), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 0ec7a47466..e89fd74ee3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -84,6 +84,9 @@ fun getActiveIconRes(blockchainId: String): Int { "canxium" -> R.drawable.img_canxium_22 "chiliz", "chiliz/test" -> R.drawable.img_chiliz_22 "clore-ai" -> R.drawable.img_clore_22 + "fact0rn" -> R.drawable.img_fact0rn_22 + "odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22 + "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 else -> R.drawable.ic_alert_24 } } @@ -166,6 +169,9 @@ fun getActiveIconResByCoinId(coinId: String): Int { "canxium" -> R.drawable.img_canxium_22 "chiliz", "chiliz/test" -> R.drawable.img_chiliz_22 "clore-ai" -> R.drawable.img_clore_22 + "fact0rn" -> R.drawable.img_fact0rn_22 + "odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22 + "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 else -> R.drawable.ic_alert_24 } } @@ -251,6 +257,9 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "canxium" -> R.drawable.ic_canxium_22 "chiliz", "chiliz/test" -> R.drawable.ic_chiliz_22 "clore-ai" -> R.drawable.ic_clore_22 + "fact0rn" -> R.drawable.ic_fact0rn_22 + "odyssey", "odyssey/test" -> R.drawable.ic_odyssey_chain_22 + "bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt deleted file mode 100644 index 17439269f9..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.core.ui.extensions - -import android.content.Context -import android.content.Intent -import androidx.core.content.ContextCompat - -fun Context.shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - ContextCompat.startActivity(this, shareIntent, null) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index 63fb8f00bd..054b2829e2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -2,12 +2,56 @@ package com.tangem.core.ui.extensions import android.graphics.Bitmap import android.graphics.Color +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalContext import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.tangem.core.res.getPluralStringSafe +import com.tangem.core.res.getStringSafe import java.util.Hashtable +/** + * Get a string resource safely or the resource name if an exception is thrown + * + * @param id resource id + * @param formatArgs format args + */ +@Composable +@ReadOnlyComposable +fun stringResourceSafe(@StringRes id: Int, vararg formatArgs: Any): String { + val resources = LocalContext.current.resources + + return if (formatArgs.isEmpty()) { + resources.getStringSafe(id) + } else { + resources.getStringSafe(id, *formatArgs) + } +} + +/** + * Get a plural string resource safely or the resource name if an exception is thrown + * + * @param id resource id + * @param count count + * @param formatArgs format args + */ +@Composable +@ReadOnlyComposable +fun pluralStringResourceSafe(@PluralsRes id: Int, count: Int, vararg formatArgs: Any): String { + val resources = LocalContext.current.resources + + return if (formatArgs.isEmpty()) { + resources.getPluralStringSafe(id, count) + } else { + resources.getPluralStringSafe(id, count, *formatArgs) + } +} + @Suppress("MagicNumber") fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { val hintMap = Hashtable() diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index efcf847a05..080fa3c043 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -6,12 +6,11 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.decapitalize +import com.tangem.core.res.getPluralStringSafe +import com.tangem.core.res.getStringSafe import org.intellij.markdown.MarkdownElementTypes import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -172,7 +171,7 @@ fun TextReference.resolveReference(): String { .map { if (it is TextReference) it.resolveReference() else it } .toTypedArray() - val resolvedReference = stringResource(id = id, *args) + val resolvedReference = stringResourceSafe(id = id, *args) if (decapitalize) { resolvedReference.replaceFirstChar { char -> char.lowercase() } @@ -180,7 +179,7 @@ fun TextReference.resolveReference(): String { resolvedReference } } - is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray()) + is TextReference.PluralRes -> pluralStringResourceSafe(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value is TextReference.Annotated -> value.text is TextReference.Combined -> { @@ -201,9 +200,9 @@ fun TextReference.resolveReference(resources: Resources): String { .map { if (it is TextReference) it.resolveReference(resources) else it } .toTypedArray() - resources.getString(id, *args) + resources.getStringSafe(id, *args) } - is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray()) + is TextReference.PluralRes -> resources.getPluralStringSafe(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value is TextReference.Annotated -> value.text is TextReference.Combined -> { @@ -225,10 +224,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString { .map { if (it is TextReference) it.resolveReference() else it } .toTypedArray() - formatAnnotated(stringResource(id = id, *args)) + formatAnnotated(stringResourceSafe(id = id, *args)) } is TextReference.PluralRes -> formatAnnotated( - pluralStringResource(id, count, *formatArgs.toTypedArray()), + pluralStringResourceSafe(id, count, *formatArgs.toTypedArray()), ) is TextReference.Str -> formatAnnotated(value) is TextReference.Annotated -> value diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt index f500f27e90..5f02c59f7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt @@ -1,11 +1,12 @@ package com.tangem.core.ui.message -import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.Composable +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable import com.tangem.core.decompose.ui.UiMessage +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage.Duration /** * Event that is used to show a message in the UI. @@ -19,29 +20,203 @@ sealed interface EventMessage : UiMessage * Shows a snackbar. * * @param message The message to show. - * @param duration The duration of the snackbar. - * @param actionLabel The label of the action button. - * @param action The action to perform when the action button is clicked. + * @param duration The duration of the snackbar. Optional, [Duration.Short] by default. + * @param onDismissRequest The action to perform when the snackbar is dismissed. Optional, empty lambda by default. + * @param actionLabel The label of the action button. Optional, `null` by default. + * @param action The action to perform when the action button is clicked. Optional, `null` by default. * */ data class SnackbarMessage( val message: TextReference, - val duration: SnackbarDuration = SnackbarDuration.Short, + val duration: Duration = Duration.Short, + val onDismissRequest: () -> Unit = {}, val actionLabel: TextReference? = null, val action: (() -> Unit)? = null, -) : EventMessage +) : EventMessage { + + /** + * The duration of the snackbar. + * */ + enum class Duration { + + /** + * Shows the snackbar for a short period of time. + * */ + Short, + + /** + * Shows the snackbar for a long period of time. + * */ + Long, + + /** + * Shows the snackbar indefinitely, until dismissed or action performed. + * */ + Indefinite, + } +} /** - * Shows a [content] in the UI. + * Shows an alert dialog. * - * @param content The content to show. + * @param message The message to show. + * @param title The title of the dialog. Optional, `null` by default. + * @param firstAction The first dialog action to perform. + * @param secondAction The second dialog action to perform. Optional, `null` by default. + * Performing the second action always dismisses the dialog. + * @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click. + * @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed, + * ignoring lambda passed to [EventMessageAction.onClick]. + * `true` by default. + * @param onDismissRequest The action to perform when the dialog is dismissed. * */ -data class ContentMessage(val content: Content) : EventMessage { +data class DialogMessage( + val message: TextReference, + val title: TextReference? = null, + val firstAction: EventMessageAction, + val secondAction: EventMessageAction? = null, + val isDismissable: Boolean = true, + val dismissOnFirstAction: Boolean = true, + val onDismissRequest: () -> Unit = {}, +) : EventMessage { - @Stable - fun interface Content { + companion object { - @Suppress("ComposableFunctionName", "TopLevelComposableFunctions") - @Composable - operator fun invoke(onDismiss: () -> Unit) + /** + * Builder for [DialogMessage]. + * + * @param message The message to show. + * @param title The title of the dialog. Optional, `null` by default. + * @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click. + * `true` by default. + * @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed, + * ignoring lambda passed to [EventMessageAction.onClick]. + * `true` by default. + * @param onDismissRequest The action to perform when the dialog is dismissed. + * @param firstActionBuilder The builder for the first action. + * By default, it builds an action with title "OK". + * @param secondActionBuilder The builder for the second action. Optional, `null` by default. + * Performing the second action always dismisses the dialog. + * + * @return [DialogMessage] with the specified parameters. + * */ + operator fun invoke( + message: TextReference, + title: TextReference? = null, + isDismissable: Boolean = true, + dismissOnFirstAction: Boolean = true, + onDismissRequest: () -> Unit = {}, + firstActionBuilder: EventMessageAction.BuilderScope.() -> EventMessageAction = { okAction() }, + secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null, + ): DialogMessage { + val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest) + + return DialogMessage( + message = message, + title = title, + isDismissable = isDismissable, + dismissOnFirstAction = dismissOnFirstAction, + onDismissRequest = onDismissRequest, + firstAction = firstActionBuilder(buttonsScope), + secondAction = secondActionBuilder?.invoke(buttonsScope), + ) + } + } +} + +/** + * Shows a bottom sheet. + * + * @param iconResId The icon to show in the bottom sheet. + * Optional, `null` by default. + * @param title The title of the bottom sheet. Optional, `null` by default. + * @param message The message to show in the bottom sheet. + * @param firstAction The first action to perform. Optional, `null` by default. + * @param secondAction The second action to perform. Optional, `null` by default. + * @param onDismissRequest The action to perform when the bottom sheet is dismissed. + * */ +data class BottomSheetMessage( + @DrawableRes val iconResId: Int? = null, + val title: TextReference? = null, + val message: TextReference, + val firstAction: EventMessageAction? = null, + val secondAction: EventMessageAction? = null, + val onDismissRequest: () -> Unit = {}, +) : EventMessage { + + companion object { + + /** + * Builder for [BottomSheetMessage]. + * + * @param iconResId The icon to show in the bottom sheet. Optional, `null` by default. + * @param title The title of the bottom sheet. Optional, `null` by default. + * @param message The message to show in the bottom sheet. + * @param onDismissRequest The action to perform when the bottom sheet is dismissed. + * @param firstActionBuilder The builder for the first action. Optional, `null` by default. + * @param secondActionBuilder The builder for the second action. Optional, `null` by default. + * */ + operator fun invoke( + @DrawableRes iconResId: Int?, + title: TextReference?, + message: TextReference, + onDismissRequest: () -> Unit = {}, + firstActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null, + secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null, + ): BottomSheetMessage { + val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest) + + return BottomSheetMessage( + iconResId = iconResId, + title = title, + message = message, + onDismissRequest = onDismissRequest, + firstAction = firstActionBuilder?.invoke(buttonsScope), + secondAction = secondActionBuilder?.invoke(buttonsScope), + ) + } + } +} + +/** + * Represents an action button in the dialog. + * + * @param title The title of the action. + * @param warning If `true` then the action is highlighted as a warning. + * @param enabled If `false` then the action is disabled. + * @param onClick The action to perform when the button is clicked. + * */ +data class EventMessageAction( + val title: TextReference, + val warning: Boolean = false, + val enabled: Boolean = true, + val onClick: () -> Unit, +) { + + /** + * Scope for building buttons for the message. + * + * @param onDismissRequest The action to perform when the dialog is dismissed. + * */ + class BuilderScope internal constructor(val onDismissRequest: () -> Unit) { + + /** + * Builds an action with title "OK". + * + * @param onClick The action to perform when the button is clicked. By default, it dismisses the message. + * */ + fun okAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onClick, + ) + + /** + * Builds an action with title "Cancel". + * + * @param onClick The action to perform when the button is clicked. By default, it dismisses the message. + * */ + fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction( + title = resourceReference(id = R.string.common_cancel), + onClick = onClick, + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt index 41475b987a..d742b59879 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt @@ -1,32 +1,137 @@ package com.tangem.core.ui.message import android.content.Context +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalEventMessageHandler +import com.tangem.core.ui.res.LocalSnackbarHostState @Composable -fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) { +fun EventMessageEffect( + messageHandler: EventMessageHandler = LocalEventMessageHandler.current, + snackbarHostState: SnackbarHostState = LocalSnackbarHostState.current, + onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context -> + showSnackbar(snackbarHostState, message, context) + }, +) { val messageEvent by messageHandler.collectAsState() val context = LocalContext.current - var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) } + + var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) } + var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) } EventEffect(event = messageEvent) { message -> when (message) { - is ContentMessage -> { - contentMessage = message - } is SnackbarMessage -> { - showSnackbar(snackbarHostState, message, context) + onShowSnackbar(message, context) + } + is DialogMessage -> { + dialogMessage = message + } + is BottomSheetMessage -> { + bottomSheetMessage = message } } } - contentMessage?.content?.invoke( - onDismiss = { contentMessage = null }, + dialogMessage?.let { message -> + MessageDialog( + message = message, + onDismissRequest = { + dialogMessage = null + message.onDismissRequest() + }, + ) + } + + bottomSheetMessage?.let { message -> + MessageBottomSheet( + message = message, + onDismissRequest = { + bottomSheetMessage = null + message.onDismissRequest() + }, + ) + } +} + +@Composable +private fun MessageBottomSheet(message: BottomSheetMessage, onDismissRequest: () -> Unit) { + val config = TangemBottomSheetConfig( + isShown = true, + content = MessageBottomSheetUM( + iconResId = message.iconResId, + title = message.title, + message = message.message, + primaryAction = message.firstAction?.let { action -> + MessageBottomSheetUM.ActionUM( + text = action.title, + enabled = action.enabled, + onClick = { + action.onClick() + onDismissRequest() + }, + ) + }, + secondaryAction = message.secondAction?.let { action -> + MessageBottomSheetUM.ActionUM( + text = action.title, + enabled = action.enabled, + onClick = { + action.onClick() + onDismissRequest() + }, + ) + }, + ), + onDismissRequest = onDismissRequest, + ) + + MessageBottomSheet(config) +} + +@Composable +private fun MessageDialog(message: DialogMessage, onDismissRequest: () -> Unit) { + BasicDialog( + message = message.message.resolveReference(), + title = message.title?.resolveReference(), + confirmButton = message.firstAction.let { action -> + DialogButtonUM( + title = action.title.resolveReference(), + warning = action.warning, + enabled = action.enabled, + onClick = { + action.onClick() + + if (message.dismissOnFirstAction) { + onDismissRequest() + } + }, + ) + }, + dismissButton = message.secondAction?.let { action -> + DialogButtonUM( + title = action.title.resolveReference(), + warning = action.warning, + enabled = action.enabled, + onClick = { + action.onClick() + onDismissRequest() + }, + ) + }, + onDismissDialog = onDismissRequest, + isDismissable = message.isDismissable, ) } @@ -34,10 +139,15 @@ private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: val result = snackbarHostState.showSnackbar( message = message.message.resolveReference(context.resources), actionLabel = message.actionLabel?.resolveReference(context.resources), - duration = message.duration, + duration = when (message.duration) { + SnackbarMessage.Duration.Short -> SnackbarDuration.Short + SnackbarMessage.Duration.Long -> SnackbarDuration.Long + SnackbarMessage.Duration.Indefinite -> SnackbarDuration.Indefinite + }, ) - if (result == SnackbarResult.ActionPerformed) { - message.action?.invoke() + when (result) { + SnackbarResult.Dismissed -> message.onDismissRequest() + SnackbarResult.ActionPerformed -> message.action?.invoke() } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 372a8c50f9..821dcd5bde 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -1,5 +1,7 @@ package com.tangem.core.ui.res +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material.Colors @@ -10,13 +12,42 @@ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.TangemShimmer import com.tangem.core.ui.haptic.DefaultHapticManager import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.VibratorHapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.windowsize.WindowSize +import com.tangem.core.ui.windowsize.rememberWindowSize +import com.tangem.domain.apptheme.model.AppThemeMode import com.valentinilk.shimmer.Shimmer +@Composable +fun TangemTheme( + activity: Activity, + uiDependencies: UiDependencies, + typography: TangemTypography = TangemTheme.typography, + dimens: TangemDimens = TangemTheme.dimens, + overrideSystemBarColors: Boolean = true, + content: @Composable () -> Unit, +) { + val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode + val windowSize = rememberWindowSize(activity = activity) + + TangemTheme( + isDark = shouldUseDarkTheme(appThemeMode), + windowSize = windowSize, + vibratorHapticManager = uiDependencies.vibratorHapticManager, + snackbarHostState = uiDependencies.globalSnackbarHostState, + eventMessageHandler = uiDependencies.eventMessageHandler, + overrideSystemBarColors = overrideSystemBarColors, + typography = typography, + dimens = dimens, + content = content, + ) +} + @Composable fun TangemTheme( isDark: Boolean = false, @@ -24,6 +55,7 @@ fun TangemTheme( typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, vibratorHapticManager: VibratorHapticManager? = null, + eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() }, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, overrideSystemBarColors: Boolean = true, content: @Composable () -> Unit, @@ -64,6 +96,7 @@ fun TangemTheme( LocalIsInDarkTheme provides isDark, LocalHapticManager provides hapticManager, LocalSnackbarHostState provides snackbarHostState, + LocalEventMessageHandler provides eventMessageHandler, LocalWindowSize provides windowSize, ) { CompositionLocalProvider( @@ -256,6 +289,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf { error("No SnackbarHostState provided") } +val LocalEventMessageHandler = staticCompositionLocalOf { + error("No EventMessageHandler provided") +} + val LocalWindowSize = staticCompositionLocalOf { error("No WindowSize provided") } @@ -266,4 +303,20 @@ val LocalTangemShimmer = staticCompositionLocalOf { val LocalMainBottomSheetColor = staticCompositionLocalOf> { error("No MainBottomSheetColor provided") +} + +/** + * Determines whether the dark theme should be used based on the given [AppThemeMode]. + * + * @param appThemeMode The application theme mode. + * @return `true` if the dark theme should be used, `false` otherwise. + */ +@Composable +@ReadOnlyComposable +private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean { + return when (appThemeMode) { + AppThemeMode.FORCE_DARK -> true + AppThemeMode.FORCE_LIGHT -> false + AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index c403c5036e..d1c1e10b32 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -2,17 +2,13 @@ package com.tangem.core.ui.screen import android.app.Activity import android.content.Context -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.windowsize.rememberWindowSize -import com.tangem.domain.apptheme.model.AppThemeMode /** * Interface representing a Compose screen with common theming and content composition properties. @@ -60,34 +56,13 @@ internal fun ComposeScreen.createComposeView( ): ComposeView { return ComposeView(context).apply { setContent { - val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode - val windowSize = rememberWindowSize(activity = activity) - TangemTheme( - isDark = shouldUseDarkTheme(appThemeMode), - windowSize = windowSize, - vibratorHapticManager = uiDependencies.vibratorHapticManager, - snackbarHostState = uiDependencies.globalSnackbarHostState, + activity = activity, + uiDependencies = uiDependencies, overrideSystemBarColors = overrideSystemBarColors, ) { ScreenContent(modifier = screenModifier) } } } -} - -/** - * Determines whether the dark theme should be used based on the given [AppThemeMode]. - * - * @param appThemeMode The application theme mode. - * @return `true` if the dark theme should be used, `false` otherwise. - */ -@Composable -@ReadOnlyComposable -private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean { - return when (appThemeMode) { - AppThemeMode.FORCE_DARK -> true - AppThemeMode.FORCE_LIGHT -> false - AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() - } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_bitrock_22.xml b/core/ui/src/main/res/drawable/ic_bitrock_22.xml new file mode 100644 index 0000000000..19a2cee307 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bitrock_22.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_cyber_22.xml b/core/ui/src/main/res/drawable/ic_cyber_22.xml index b622330ee9..bf79d52477 100644 --- a/core/ui/src/main/res/drawable/ic_cyber_22.xml +++ b/core/ui/src/main/res/drawable/ic_cyber_22.xml @@ -3,15 +3,15 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - - - - - - + + diff --git a/core/ui/src/main/res/drawable/ic_fact0rn_22.xml b/core/ui/src/main/res/drawable/ic_fact0rn_22.xml new file mode 100644 index 0000000000..2c71132ba0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fact0rn_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_odyssey_chain_22.xml b/core/ui/src/main/res/drawable/ic_odyssey_chain_22.xml new file mode 100644 index 0000000000..a7c93ad394 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_odyssey_chain_22.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_bitrock_22.xml b/core/ui/src/main/res/drawable/img_bitrock_22.xml new file mode 100644 index 0000000000..0b00d94c0d --- /dev/null +++ b/core/ui/src/main/res/drawable/img_bitrock_22.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_card_visa.webp b/core/ui/src/main/res/drawable/img_card_visa.webp new file mode 100644 index 0000000000..bd2804ca99 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_card_visa.webp differ diff --git a/core/ui/src/main/res/drawable/img_fact0rn_22.xml b/core/ui/src/main/res/drawable/img_fact0rn_22.xml new file mode 100644 index 0000000000..db98cf00a7 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_fact0rn_22.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_odyssey_chain_22.xml b/core/ui/src/main/res/drawable/img_odyssey_chain_22.xml new file mode 100644 index 0000000000..b4f62cdb25 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_odyssey_chain_22.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/ProviderSuspend.kt b/core/utils/src/main/java/com/tangem/utils/ProviderSuspend.kt new file mode 100644 index 0000000000..47a25d101e --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/ProviderSuspend.kt @@ -0,0 +1,10 @@ +package com.tangem.utils + +/** + * Provider for suspend lazy initialization + * + * @param action initialization action + * +[REDACTED_AUTHOR] + */ +class ProviderSuspend(action: suspend () -> T) : suspend () -> T by action \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index cfa0536804..d0b50ceab4 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -25,11 +25,12 @@ class Debouncer { coroutineScope: CoroutineScope, waitMs: Long = 300L, context: CoroutineContext = EmptyCoroutineContext, + forceUpdate: Boolean = false, destinationFunction: suspend () -> Unit, ) { debounceJob?.cancel() debounceJob = coroutineScope.launch(context) { - delay(waitMs) + if (!forceUpdate) delay(waitMs) destinationFunction.invoke() } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 4b02448af9..594efd38f4 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -257,6 +257,7 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet, Blockchain.Radiant, + Blockchain.Fact0rn, Blockchain.Base, Blockchain.BaseTestnet, Blockchain.Moonriver, @@ -289,6 +290,10 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra Blockchain.Canxium, Blockchain.Chiliz, Blockchain.ChilizTestnet, + Blockchain.VanarChain, + Blockchain.VanarChainTestnet, + Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet, + Blockchain.Bitrock, Blockchain.BitrockTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/promo/build.gradle.kts b/data/promo/build.gradle.kts index b11dc26ed9..03bcb7be3b 100644 --- a/data/promo/build.gradle.kts +++ b/data/promo/build.gradle.kts @@ -17,8 +17,9 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) + implementation(projects.domain.promo) + implementation(projects.domain.promo.models) + implementation(projects.domain.wallets.models) implementation(projects.core.datasource) implementation(projects.core.utils) diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 0a81c2200c..6a0307943f 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -3,13 +3,27 @@ package com.tangem.data.promo import com.tangem.data.promo.converters.PromoResponseConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.promo.PromoBanner -import com.tangem.domain.tokens.repository.PromoRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.models.PromoBanner +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map internal class DefaultPromoRepository( private val tangemApi: TangemTechApi, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : PromoRepository { @@ -41,6 +55,57 @@ internal class DefaultPromoRepository( }.getOrNull() } + override fun isReadyToShowWalletSwapPromo(): Flow { + return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true) + } + + override fun isReadyToShowTokenSwapPromo(): Flow { + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true) + } + + override suspend fun setNeverToShowWalletSwapPromo() { + appPreferencesStore.store( + key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, + value = false, + ) + } + + override suspend fun setNeverToShowTokenSwapPromo() { + appPreferencesStore.store( + key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, + value = false, + ) + } + + override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow { + return combine( + flow = appPreferencesStore + .get(key = ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) + .map { userWalletId.stringValue in it }, + flow2 = appPreferencesStore.get(key = SHOULD_SHOW_RING_PROMO_KEY, default = true), + transform = { isRingAdded, shouldShowRingPromo -> isRingAdded && shouldShowRingPromo }, + ) + } + + override suspend fun setNeverToShowRingPromo() { + appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false) + } + + override fun isReadyToShowSwapStories(): Flow { + return appPreferencesStore.get(SHOULD_SHOW_SWAP_STORIES_KEY, true) + } + + override suspend fun isReadyToShowSwapStoriesSync(): Boolean { + return appPreferencesStore.getSyncOrDefault(SHOULD_SHOW_SWAP_STORIES_KEY, true) + } + + override suspend fun setNeverToShowSwapStories() { + appPreferencesStore.store( + key = SHOULD_SHOW_SWAP_STORIES_KEY, + value = false, + ) + } + private companion object { private const val CHANGELLY_NAME = "changelly" private const val OKX = "okx" diff --git a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt index 151675506b..cc418e808c 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.promo.converters import com.tangem.datasource.api.promotion.models.PromotionInfoResponse -import com.tangem.domain.promo.PromoBanner +import com.tangem.domain.promo.models.PromoBanner import com.tangem.utils.converter.Converter import org.joda.time.DateTime diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 5ed2c6855f..102afaa289 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -2,7 +2,8 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.tokens.repository.PromoRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.promo.PromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -18,8 +19,9 @@ internal object PromoDataModule { @Singleton fun providePromoRepository( tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): PromoRepository { - return DefaultPromoRepository(tangemTechApi, dispatchers) + return DefaultPromoRepository(tangemTechApi, appPreferencesStore, dispatchers) } } \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 992a502c03..6856b1236f 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.settings) - implementation(projects.domain.wallets.models) implementation(deps.androidx.datastore) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt deleted file mode 100644 index 8a2b5aa1d9..0000000000 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.data.settings - -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY -import com.tangem.datasource.local.preferences.utils.get -import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.settings.repositories.PromoSettingsRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -/** - * Repository for showing swap promo notification. - */ -class DefaultPromoSettingsRepository( - private val appPreferencesStore: AppPreferencesStore, -) : PromoSettingsRepository { - override fun isReadyToShowWalletSwapPromo(): Flow { - return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true) - } - - override fun isReadyToShowTokenSwapPromo(): Flow { - return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true) - } - - override suspend fun setNeverToShowWalletSwapPromo() { - appPreferencesStore.store( - key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, - value = false, - ) - } - - override suspend fun setNeverToShowTokenSwapPromo() { - appPreferencesStore.store( - key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, - value = false, - ) - } - - override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow { - return combine( - flow = appPreferencesStore - .get(key = ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) - .map { userWalletId.stringValue in it }, - flow2 = appPreferencesStore.get(key = SHOULD_SHOW_RING_PROMO_KEY, default = true), - transform = { isRingAdded, shouldShowRingPromo -> isRingAdded && shouldShowRingPromo }, - ) - } - - override suspend fun setNeverToShowRingPromo() { - appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false) - } -} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 3c6c740e2e..789406f504 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -12,6 +12,7 @@ import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -127,6 +128,8 @@ internal class DefaultSettingsRepository( return null } + override fun getUserCountryCode(): StateFlow = userCountryFlow + override suspend fun fetchUserCountryCode() { Timber.i("Start fetching user country code") diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 0ea5bce762..4f04ee135b 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -2,14 +2,12 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultPermissionRepository -import com.tangem.data.settings.DefaultPromoSettingsRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PermissionRepository -import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -44,12 +42,6 @@ internal object SettingsDataModule { return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore) } - @Provides - @Singleton - fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository { - return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore) - } - @Provides @Singleton fun providePushPermissionRepository(appPreferencesStore: AppPreferencesStore): PermissionRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt new file mode 100644 index 0000000000..a5be656c53 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -0,0 +1,28 @@ +package com.tangem.data.visa + +import com.tangem.data.visa.converter.VisaActivationStatusConverter +import com.tangem.datasource.api.visa.TangemVisaApi +import com.tangem.domain.visa.model.ActivationOrder +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultVisaActivationRepository @Inject constructor( + private val visaApi: TangemVisaApi, + private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaActivationStatusConverter: VisaActivationStatusConverter, +) : VisaActivationRepository { + + override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { + visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus()) + // TODO implement refreshing access token if it's expired + } + + override suspend fun getActivationOrderToSign(): ActivationOrder { + return ActivationOrder("TODO implement") + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt new file mode 100644 index 0000000000..a0fb323d81 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -0,0 +1,70 @@ +package com.tangem.data.visa + +import com.tangem.datasource.api.visa.TangemVisaAuthApi +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSession +import com.tangem.domain.visa.model.VisaAuthSignedChallenge +import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultVisaAuthRepository @Inject constructor( + private val visaAuthApi: TangemVisaAuthApi, + private val dispatchers: CoroutineDispatcherProvider, +) : VisaAuthRepository { + + override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card = + withContext(dispatchers.io) { + val response = visaAuthApi.generateNonceByCard( + cardId = cardId, + cardPublicKey = cardPublicKey, + ) + + VisaAuthChallenge.Card( + challenge = response.nonce, + session = VisaAuthSession(response.sessionId), + ) + } + + override suspend fun getCustomerWalletAuthChallenge( + cardId: String, + walletPublicKey: String, + ): VisaAuthChallenge.Wallet = withContext(dispatchers.io) { + val response = visaAuthApi.generateNonceByWalletAddress( + customerId = cardId, + customerWalletAddress = walletPublicKey, + ) + + VisaAuthChallenge.Wallet( + challenge = response.nonce, + session = VisaAuthSession(response.sessionId), + ) + } + + override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens = + withContext(dispatchers.io) { + val response = when (signedChallenge) { + is VisaAuthSignedChallenge.ByCardPublicKey -> { + visaAuthApi.getAccessToken( + sessionId = signedChallenge.challenge.session.sessionId, + signature = signedChallenge.signature, + salt = signedChallenge.salt, + ) + } + is VisaAuthSignedChallenge.ByWallet -> { + visaAuthApi.getAccessToken( + sessionId = signedChallenge.challenge.session.sessionId, + signature = signedChallenge.signature, + salt = null, + ) + } + } + + VisaAuthTokens( + accessToken = response.accessToken, + refreshToken = response.refreshToken, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt new file mode 100644 index 0000000000..d82a09116a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.data.visa.converter + +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class VisaActivationStatusConverter @Inject constructor() : + Converter { + + override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState { + // TODO Will be implemented in the future + return VisaActivationRemoteState.Activated + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a8587868f9..1ffd8cec35 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,7 +1,12 @@ package com.tangem.data.visa.di +import com.tangem.data.visa.DefaultVisaActivationRepository +import com.tangem.data.visa.DefaultVisaAuthRepository import com.tangem.data.visa.DummyVisaRepository +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaRepository +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,4 +26,17 @@ internal object VisaDataModule { ): VisaRepository { return implementedVisaRepository.getOrNull() ?: DummyVisaRepository() } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface VisaDataBindsModule { + + @Binds + @Singleton + fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository + + @Binds + @Singleton + fun bindVisaActivationRepository(repository: DefaultVisaActivationRepository): VisaActivationRepository } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 2839cb01c0..570f0f9b56 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.feedback import android.content.res.Resources +import com.tangem.core.res.getStringSafe import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmail import com.tangem.domain.feedback.models.FeedbackEmailType @@ -59,6 +60,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.ScanningProblem, is FeedbackEmailType.CurrencyDescriptionError, is FeedbackEmailType.PreActivatedWallet, + is FeedbackEmailType.CardAttestationFailed, -> this is FeedbackEmailType.DirectUserRequest, is FeedbackEmailType.RateCanBeBetter, @@ -66,7 +68,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.SwapProblem, is FeedbackEmailType.TransactionSendingProblem, -> { - append(resources.getString(R.string.feedback_data_collection_message)) + append(resources.getStringSafe(R.string.feedback_data_collection_message)) skipLine() } } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 2ec928f7ab..364c363194 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -48,4 +48,8 @@ sealed interface FeedbackEmailType { } data class PreActivatedWallet(override val cardInfo: CardInfo) : FeedbackEmailType + + data object CardAttestationFailed : FeedbackEmailType { + override val cardInfo: CardInfo? = null + } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 9983e4bcc4..e531f71d3f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -21,12 +21,14 @@ internal class EmailMessageBodyResolver( when (type) { is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo) is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) - is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type) is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type) is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type) is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.ScanningProblem, + is FeedbackEmailType.CardAttestationFailed, + -> addPhoneInfoBody() } return build() @@ -50,7 +52,7 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } - private fun FeedbackDataBuilder.addScanningProblemBody() { + private fun FeedbackDataBuilder.addPhoneInfoBody() { addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 1311a3016e..30058b611e 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -1,6 +1,7 @@ package com.tangem.domain.feedback.utils import android.content.res.Resources +import com.tangem.core.res.getStringSafe import com.tangem.domain.feedback.R import com.tangem.domain.feedback.models.FeedbackEmailType @@ -18,6 +19,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { return when (type) { is FeedbackEmailType.DirectUserRequest, is FeedbackEmailType.CurrencyDescriptionError, + is FeedbackEmailType.CardAttestationFailed, -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed @@ -27,6 +29,6 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { -> R.string.feedback_preface_tx_failed is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_preface_support } - .let(resources::getString) + .let(resources::getStringSafe) } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index e373d8d496..cc2b6c379c 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -1,6 +1,7 @@ package com.tangem.domain.feedback.utils import android.content.res.Resources +import com.tangem.core.res.getStringSafe import com.tangem.domain.feedback.R import com.tangem.domain.feedback.models.FeedbackEmailType @@ -18,20 +19,24 @@ internal class EmailSubjectResolver(private val resources: Resources) { return when (type) { is FeedbackEmailType.DirectUserRequest -> { if (type.cardInfo.isStart2Coin) { - R.string.feedback_subject_support + resources.getStringSafe(R.string.feedback_subject_support) } else { - R.string.feedback_subject_support_tangem + resources.getStringSafe(R.string.feedback_subject_support_tangem) } } - is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative - is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + is FeedbackEmailType.RateCanBeBetter -> resources.getStringSafe(R.string.feedback_subject_rate_negative) + is FeedbackEmailType.ScanningProblem -> resources.getStringSafe(R.string.feedback_subject_scan_failed) is FeedbackEmailType.TransactionSendingProblem, is FeedbackEmailType.StakingProblem, is FeedbackEmailType.SwapProblem, - -> R.string.feedback_subject_tx_failed - is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_subject_pre_activated_wallet - is FeedbackEmailType.CurrencyDescriptionError -> R.string.feedback_token_description_error + -> resources.getStringSafe(R.string.feedback_subject_tx_failed) + is FeedbackEmailType.PreActivatedWallet -> { + resources.getStringSafe(R.string.feedback_subject_pre_activated_wallet) + } + is FeedbackEmailType.CurrencyDescriptionError -> { + resources.getStringSafe(R.string.feedback_token_description_error) + } + FeedbackEmailType.CardAttestationFailed -> "Card attestation failed" } - .let(resources::getString) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 85f6165a23..bcf606539a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.operations.attestation.Attestation @@ -66,7 +67,7 @@ internal class TangemCardTypesResolver( override fun getBlockchain(): Blockchain { return when (productType) { ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin - ProductType.Visa -> Blockchain.PolygonTestnet + ProductType.Visa -> VisaUtilities.visaBlockchain else -> { val blockchainName: String = walletData?.blockchain ?: if (productType == ProductType.Note) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 601ecc526b..b369611972 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -2,6 +2,7 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO import java.util.Locale @@ -23,7 +24,7 @@ object TapWorkarounds { get() = isStart2CoinIssuer(issuer.name) val CardDTO.isVisa: Boolean - get() = firmwareVersion.doubleValue in FirmwareVersion.visaRange + get() = VisaUtilities.isVisaCard(this) val CardDTO.isTestCard: Boolean get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt index 0e02f053df..baeb91fbed 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import timber.log.Timber -object Wallet2CardConfig : CardConfig { +data object Wallet2CardConfig : CardConfig { override val mandatoryCurves: List get() = listOf( EllipticCurve.Secp256k1, @@ -20,24 +20,180 @@ object Wallet2CardConfig : CardConfig { */ override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { // order is important, new curve is preferred for wallet 2 - return when { - blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519Slip0010) -> { - EllipticCurve.Ed25519Slip0010 - } - blockchain.getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { - EllipticCurve.Secp256k1 - } - blockchain.getSupportedCurves().contains(EllipticCurve.Bls12381G2Aug) -> { - EllipticCurve.Bls12381G2Aug - } - // only for support cardano on Wallet2 - blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519) -> { - EllipticCurve.Ed25519 - } - else -> { - Timber.e("Unsupported blockchain, curve not found") - null - } + // TODO Comment old logic without direct mapping until tests and release + // return when { + // blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519Slip0010) -> { + // EllipticCurve.Ed25519Slip0010 + // } + // blockchain.getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + // EllipticCurve.Secp256k1 + // } + // blockchain.getSupportedCurves().contains(EllipticCurve.Bls12381G2Aug) -> { + // EllipticCurve.Bls12381G2Aug + // } + // // only for support cardano on Wallet2 + // blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + // EllipticCurve.Ed25519 + // } + // else -> { + // Timber.e("Unsupported blockchain, curve not found") + // null + // } + // } + val curve = getPrimaryCurveForBlockchain(blockchain) + // check curve supports + if (!blockchain.getSupportedCurves().contains(curve)) { + Timber.e("Unsupported curve $curve for blockchain $blockchain") + return null + } + return curve + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun getPrimaryCurveForBlockchain(blockchain: Blockchain): EllipticCurve? { + return when (blockchain) { + Blockchain.Unknown -> null + Blockchain.Arbitrum -> EllipticCurve.Secp256k1 + Blockchain.ArbitrumTestnet -> EllipticCurve.Secp256k1 + Blockchain.Avalanche -> EllipticCurve.Secp256k1 + Blockchain.AvalancheTestnet -> EllipticCurve.Secp256k1 + Blockchain.Binance -> EllipticCurve.Secp256k1 + Blockchain.BinanceTestnet -> EllipticCurve.Secp256k1 + Blockchain.BSC -> EllipticCurve.Secp256k1 + Blockchain.BSCTestnet -> EllipticCurve.Secp256k1 + Blockchain.Bitcoin -> EllipticCurve.Secp256k1 + Blockchain.BitcoinTestnet -> EllipticCurve.Secp256k1 + Blockchain.BitcoinCash -> EllipticCurve.Secp256k1 + Blockchain.BitcoinCashTestnet -> EllipticCurve.Secp256k1 + Blockchain.Cardano -> EllipticCurve.Ed25519 + Blockchain.Cosmos -> EllipticCurve.Secp256k1 + Blockchain.CosmosTestnet -> EllipticCurve.Secp256k1 + Blockchain.Dogecoin -> EllipticCurve.Secp256k1 + Blockchain.Ducatus -> EllipticCurve.Secp256k1 + Blockchain.Ethereum -> EllipticCurve.Secp256k1 + Blockchain.EthereumTestnet -> EllipticCurve.Secp256k1 + Blockchain.EthereumClassic -> EllipticCurve.Secp256k1 + Blockchain.EthereumClassicTestnet -> EllipticCurve.Secp256k1 + Blockchain.Fantom -> EllipticCurve.Secp256k1 + Blockchain.FantomTestnet -> EllipticCurve.Secp256k1 + Blockchain.Litecoin -> EllipticCurve.Secp256k1 + Blockchain.Near -> EllipticCurve.Ed25519Slip0010 + Blockchain.NearTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Polkadot -> EllipticCurve.Ed25519Slip0010 + Blockchain.PolkadotTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Kava -> EllipticCurve.Secp256k1 + Blockchain.KavaTestnet -> EllipticCurve.Secp256k1 + Blockchain.Kusama -> EllipticCurve.Ed25519Slip0010 + Blockchain.Polygon -> EllipticCurve.Secp256k1 + Blockchain.PolygonTestnet -> EllipticCurve.Secp256k1 + Blockchain.RSK -> EllipticCurve.Secp256k1 + Blockchain.Sei -> EllipticCurve.Secp256k1 + Blockchain.SeiTestnet -> EllipticCurve.Secp256k1 + Blockchain.Stellar -> EllipticCurve.Ed25519Slip0010 + Blockchain.StellarTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Solana -> EllipticCurve.Ed25519Slip0010 + Blockchain.SolanaTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Tezos -> EllipticCurve.Ed25519Slip0010 + Blockchain.Tron -> EllipticCurve.Secp256k1 + Blockchain.TronTestnet -> EllipticCurve.Secp256k1 + Blockchain.XRP -> EllipticCurve.Secp256k1 + Blockchain.Gnosis -> EllipticCurve.Secp256k1 + Blockchain.Dash -> EllipticCurve.Secp256k1 + Blockchain.Optimism -> EllipticCurve.Secp256k1 + Blockchain.OptimismTestnet -> EllipticCurve.Secp256k1 + Blockchain.Dischain -> EllipticCurve.Secp256k1 + Blockchain.EthereumPow -> EllipticCurve.Secp256k1 + Blockchain.EthereumPowTestnet -> EllipticCurve.Secp256k1 + Blockchain.Kaspa -> EllipticCurve.Secp256k1 + Blockchain.Telos -> EllipticCurve.Secp256k1 + Blockchain.TelosTestnet -> EllipticCurve.Secp256k1 + Blockchain.TON -> EllipticCurve.Ed25519Slip0010 + Blockchain.TONTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Ravencoin -> EllipticCurve.Secp256k1 + Blockchain.RavencoinTestnet -> EllipticCurve.Secp256k1 + Blockchain.TerraV1 -> EllipticCurve.Secp256k1 + Blockchain.TerraV2 -> EllipticCurve.Secp256k1 + Blockchain.Cronos -> EllipticCurve.Secp256k1 + Blockchain.AlephZero -> EllipticCurve.Ed25519Slip0010 + Blockchain.AlephZeroTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.OctaSpace -> EllipticCurve.Secp256k1 + Blockchain.OctaSpaceTestnet -> EllipticCurve.Secp256k1 + Blockchain.Chia -> EllipticCurve.Bls12381G2Aug + Blockchain.ChiaTestnet -> EllipticCurve.Bls12381G2Aug + Blockchain.Decimal -> EllipticCurve.Secp256k1 + Blockchain.DecimalTestnet -> EllipticCurve.Secp256k1 + Blockchain.XDC -> EllipticCurve.Secp256k1 + Blockchain.XDCTestnet -> EllipticCurve.Secp256k1 + Blockchain.VeChain -> EllipticCurve.Secp256k1 + Blockchain.VeChainTestnet -> EllipticCurve.Secp256k1 + Blockchain.Aptos -> EllipticCurve.Ed25519Slip0010 + Blockchain.AptosTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Playa3ull -> EllipticCurve.Secp256k1 + Blockchain.Shibarium -> EllipticCurve.Secp256k1 + Blockchain.ShibariumTestnet -> EllipticCurve.Secp256k1 + Blockchain.Algorand -> EllipticCurve.Ed25519Slip0010 + Blockchain.AlgorandTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Hedera -> EllipticCurve.Ed25519Slip0010 + Blockchain.HederaTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Aurora -> EllipticCurve.Secp256k1 + Blockchain.AuroraTestnet -> EllipticCurve.Secp256k1 + Blockchain.Areon -> EllipticCurve.Secp256k1 + Blockchain.AreonTestnet -> EllipticCurve.Secp256k1 + Blockchain.PulseChain -> EllipticCurve.Secp256k1 + Blockchain.PulseChainTestnet -> EllipticCurve.Secp256k1 + Blockchain.ZkSyncEra -> EllipticCurve.Secp256k1 + Blockchain.ZkSyncEraTestnet -> EllipticCurve.Secp256k1 + Blockchain.Nexa -> EllipticCurve.Secp256k1 + Blockchain.NexaTestnet -> EllipticCurve.Secp256k1 + Blockchain.Moonbeam -> EllipticCurve.Secp256k1 + Blockchain.MoonbeamTestnet -> EllipticCurve.Secp256k1 + Blockchain.Manta -> EllipticCurve.Secp256k1 + Blockchain.MantaTestnet -> EllipticCurve.Secp256k1 + Blockchain.PolygonZkEVM -> EllipticCurve.Secp256k1 + Blockchain.PolygonZkEVMTestnet -> EllipticCurve.Secp256k1 + Blockchain.Radiant -> EllipticCurve.Secp256k1 + Blockchain.Base -> EllipticCurve.Secp256k1 + Blockchain.BaseTestnet -> EllipticCurve.Secp256k1 + Blockchain.Moonriver -> EllipticCurve.Secp256k1 + Blockchain.MoonriverTestnet -> EllipticCurve.Secp256k1 + Blockchain.Mantle -> EllipticCurve.Secp256k1 + Blockchain.MantleTestnet -> EllipticCurve.Secp256k1 + Blockchain.Fact0rn -> EllipticCurve.Secp256k1 + Blockchain.Flare -> EllipticCurve.Secp256k1 + Blockchain.FlareTestnet -> EllipticCurve.Secp256k1 + Blockchain.Taraxa -> EllipticCurve.Secp256k1 + Blockchain.TaraxaTestnet -> EllipticCurve.Secp256k1 + Blockchain.Koinos -> EllipticCurve.Secp256k1 + Blockchain.KoinosTestnet -> EllipticCurve.Secp256k1 + Blockchain.Joystream -> EllipticCurve.Ed25519Slip0010 + Blockchain.Bittensor -> EllipticCurve.Ed25519Slip0010 + Blockchain.Filecoin -> EllipticCurve.Secp256k1 + Blockchain.Blast -> EllipticCurve.Secp256k1 + Blockchain.BlastTestnet -> EllipticCurve.Secp256k1 + Blockchain.Cyber -> EllipticCurve.Secp256k1 + Blockchain.CyberTestnet -> EllipticCurve.Secp256k1 + Blockchain.InternetComputer -> EllipticCurve.Secp256k1 + Blockchain.Sui -> EllipticCurve.Ed25519Slip0010 + Blockchain.SuiTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.EnergyWebChain -> EllipticCurve.Secp256k1 + Blockchain.EnergyWebChainTestnet -> EllipticCurve.Secp256k1 + Blockchain.EnergyWebX -> EllipticCurve.Ed25519Slip0010 + Blockchain.EnergyWebXTestnet -> EllipticCurve.Ed25519Slip0010 + Blockchain.Casper -> EllipticCurve.Secp256k1 + Blockchain.CasperTestnet -> EllipticCurve.Secp256k1 + Blockchain.Core -> EllipticCurve.Secp256k1 + Blockchain.CoreTestnet -> EllipticCurve.Secp256k1 + Blockchain.Xodex -> EllipticCurve.Secp256k1 + Blockchain.Canxium -> EllipticCurve.Secp256k1 + Blockchain.Chiliz -> EllipticCurve.Secp256k1 + Blockchain.ChilizTestnet -> EllipticCurve.Secp256k1 + Blockchain.Clore -> EllipticCurve.Secp256k1 + Blockchain.VanarChain -> EllipticCurve.Secp256k1 + Blockchain.VanarChainTestnet -> EllipticCurve.Secp256k1 + Blockchain.OdysseyChain -> EllipticCurve.Secp256k1 + Blockchain.OdysseyChainTestnet -> EllipticCurve.Secp256k1 + Blockchain.Bitrock -> EllipticCurve.Secp256k1 + Blockchain.BitrockTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt new file mode 100644 index 0000000000..32d4282b20 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.common.visa + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.models.scan.CardDTO + +private const val VISA_BATCH_START = "AE" + +object VisaUtilities { + + val mandatoryCurve = EllipticCurve.Secp256k1 + + const val tokenId = "tether" + + val visaBlockchain = Blockchain.Polygon + + val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3) + + fun isVisaCard(card: CardDTO): Boolean { + return isVisaCard(card.firmwareVersion.doubleValue, card.batchId) + } + + fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean { + return FirmwareVersion.visaRange.contains(firmwareVersion) && + batchId.startsWith(VISA_BATCH_START) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 0adb568906..3e6ad8a4e8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -1,9 +1,11 @@ package com.tangem.domain.exchange +import arrow.core.Either import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -12,9 +14,6 @@ import kotlinx.coroutines.flow.Flow */ interface RampStateManager { - /** Check if sell service is supported for given [CryptoCurrency] */ - fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean - suspend fun availableForBuy( scanResponse: ScanResponse, userWalletId: UserWalletId, @@ -24,10 +23,13 @@ interface RampStateManager { /** * Check if [CryptoCurrency] is available for sell * - * @param userWalletId id of multi-currency wallet - * @param status crypto currency status + * @param userWalletId id of multi-currency wallet + * @param status crypto currency status */ - suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean + suspend fun availableForSell( + userWalletId: UserWalletId, + status: CryptoCurrencyStatus, + ): Either suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt deleted file mode 100644 index 67be91d2dd..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.domain.redux - -import com.tangem.domain.redux.global.DomainGlobalState -import org.rekotlin.StateType - -/** -[REDACTED_AUTHOR] - */ -data class DomainState(val globalState: DomainGlobalState = DomainGlobalState()) : StateType \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt deleted file mode 100644 index a141cd5f9a..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.domain.redux - -import com.tangem.domain.redux.global.DomainGlobalHub -import org.rekotlin.Action -import org.rekotlin.Store - -/** -[REDACTED_AUTHOR] - */ -private val RE_STORE_HUBS: List> = listOf(DomainGlobalHub()) - -val domainStore = Store( - state = DomainState(), - middleware = RE_STORE_HUBS.map { it.getMiddleware() }, - reducer = { action, state -> reduce(action, state) }, -) - -private fun reduce(action: Action, domainState: DomainState?): DomainState { - requireNotNull(domainState) - - // we can examine the store state after each change by reducer - var assembleReducedDomainState: DomainState = domainState - val reducedStatesByAction = mutableListOf>() - - RE_STORE_HUBS.forEach { - val reducedState = it.reduce(action, assembleReducedDomainState) - - assembleReducedDomainState = if (reducedState != assembleReducedDomainState) { - reducedStatesByAction.add(action to assembleReducedDomainState) - reducedState - } else { - assembleReducedDomainState - } - } - - return assembleReducedDomainState -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReStoreHub.kt deleted file mode 100644 index fcef0871d2..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReStoreHub.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.tangem.domain.redux - -import android.webkit.ValueCallback -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.job -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.DispatchFunction -import org.rekotlin.Middleware -import java.util.concurrent.Executors - -/** -[REDACTED_AUTHOR] - * ReStoreHub's should not store the or the , because this can lead to destabilization of - * a state behavior. - * All ReStoreHub's must be marked as internal - */ -internal interface ReStoreHub { - fun getMiddleware(): Middleware - fun reduce(action: Action, domainState: StoreState): StoreState -} - -internal interface ReStoreReducer { - fun reduceAction(action: Action, state: State): State -} - -/** - * ReStoreHub is the entry point for actions. It processes it through middleware and reducer. - * Actions handled by ReStoreHub go into coroutine scope, which can be canceled while the action is being processed. - * All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent - * concurrent modification in the Store - * Only the changed hub State will change its state in the DomainState - * Do not implement other states like as DomainGlobalState. Because it can dilute the responsibility of - * states. - * @param name - name of the Hub - * @param dispatcher - main coroutine dispatcher for actions - * @property globalState - state witch produce accessibility to global variables - */ -internal abstract class BaseStoreHub( - private val name: String, - private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(), -) : ReStoreHub { - - val globalState: DomainGlobalState - get() = domainStore.state.globalState - - val hubScope = CoroutineScope( - Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name), - ) - - private val actionsAndJobs = mutableMapOf() - - override fun getMiddleware(): Middleware { - return { dispatch, state -> - { next -> - { action -> - handle(state, action, dispatch) - next(action) - } - } - } - } - - /** - * Launches new coroutine and stores the action with it's coroutine job. (Coroutine can be cancelled - * through invoking the cancelActionJob() function inside a middleware). - * Removes the action when job is completed. - */ - protected open fun handle(storeStateHolder: () -> DomainState?, action: Action, dispatch: DispatchFunction) { - val storeState = storeStateHolder() - ?: throw UnsupportedOperationException("StoreState for the $name can't be NULL") - - hubScope.launch { - actionsAndJobs[action] = this.coroutineContext.job - actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) } - - handleAction(action, storeState) { - actionsAndJobs.remove(it)?.cancel() - } - } - } - - /** - * Reduce the action and check it. If the action hasn't updated the hubState, then it doesn't need to update - * storeState - */ - override fun reduce(action: Action, domainState: DomainState): DomainState { - val hubOldState = getHubState(domainState) - val hubNewState = getReducer().reduceAction(action, hubOldState) - return if (hubOldState === hubNewState) { - domainState - } else { - updateStoreState(domainState, hubNewState) - } - } - - protected fun cancelAll() { - actionsAndJobs.forEach { (_, job) -> job.cancel() } - } - - protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) - protected abstract fun getReducer(): ReStoreReducer - - protected abstract fun getHubState(storeState: DomainState): State - protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt deleted file mode 100644 index 36db6ef7ed..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.redux.extensions - -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.redux.domainStore -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -internal suspend inline fun dispatchOnMain(vararg actions: Action) { - withMainContext { actions.forEach { domainStore.dispatch(it) } } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt deleted file mode 100644 index 90bf41578e..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.redux.global - -import com.tangem.domain.models.scan.ScanResponse -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -// TODO: refactoring: is alias for the GlobalAction -sealed class DomainGlobalAction : Action { - data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction() -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt deleted file mode 100644 index 18f081f8a7..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.domain.redux.global - -import android.webkit.ValueCallback -import com.tangem.common.extensions.toHexString -import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.utils.RequestHeader -import com.tangem.domain.redux.BaseStoreHub -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.ReStoreReducer -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -// TODO: refactoring: is alias for the GlobalMiddleware and the GlobalReducer -internal class DomainGlobalHub : BaseStoreHub("DomainGlobalHub") { - - override fun getHubState(storeState: DomainState): DomainGlobalState { - return storeState.globalState - } - - override fun updateStoreState(storeState: DomainState, newHubState: DomainGlobalState): DomainState { - return storeState.copy(globalState = newHubState) - } - - override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) { - if (action !is DomainGlobalAction) return - } - - override fun getReducer(): ReStoreReducer = DomainGlobalReducer() -} - -private class DomainGlobalReducer : ReStoreReducer { - - override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState { - return when (action) { - is DomainGlobalAction.SaveScanNoteResponse -> { - val card = action.scanResponse.card - // TODO: AuthHeaders: try to remove it, because now we can use headers with dynamic values - state.networkServices.tangemTechService.addAuthenticationHeader( - RequestHeader.AuthenticationHeader( - object : AuthProvider { - override fun getCardPublicKey(): String = card.cardPublicKey.toHexString() - override fun getCardId(): String = card.cardId - }, - ), - ) - state.copy(scanResponse = action.scanResponse) - } - else -> state - } - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt deleted file mode 100644 index bd6efc230c..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.redux.global - -import com.tangem.datasource.api.tangemTech.TangemTechService -import com.tangem.domain.models.scan.ScanResponse - -/** -[REDACTED_AUTHOR] - */ -data class DomainGlobalState( - // there is a part of mirrors from the GlobalState. - // It updates on GlobalAction.SaveScanNoteResponse -> DomainGlobalAction.SaveScanNoteResponse(scanResponse) - val scanResponse: ScanResponse? = null, - // - val networkServices: NetworkServices = NetworkServices(), -) - -data class NetworkServices( - val tangemTechService: TangemTechService = TangemTechService, -) \ No newline at end of file diff --git a/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt new file mode 100644 index 0000000000..5ab98a3e72 --- /dev/null +++ b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt @@ -0,0 +1,163 @@ +package com.tangem.domain.common.configs + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class Wallet2CardConfigTest { + + private val wallet2CardConfig = Wallet2CardConfig + private val expectedMapping = mapOf( + Blockchain.Unknown to null, + Blockchain.Arbitrum to EllipticCurve.Secp256k1, + Blockchain.ArbitrumTestnet to EllipticCurve.Secp256k1, + Blockchain.Avalanche to EllipticCurve.Secp256k1, + Blockchain.AvalancheTestnet to EllipticCurve.Secp256k1, + Blockchain.Binance to EllipticCurve.Secp256k1, + Blockchain.BinanceTestnet to EllipticCurve.Secp256k1, + Blockchain.BSC to EllipticCurve.Secp256k1, + Blockchain.BSCTestnet to EllipticCurve.Secp256k1, + Blockchain.Bitcoin to EllipticCurve.Secp256k1, + Blockchain.BitcoinTestnet to EllipticCurve.Secp256k1, + Blockchain.BitcoinCash to EllipticCurve.Secp256k1, + Blockchain.BitcoinCashTestnet to EllipticCurve.Secp256k1, + Blockchain.Cardano to EllipticCurve.Ed25519, + Blockchain.Cosmos to EllipticCurve.Secp256k1, + Blockchain.CosmosTestnet to EllipticCurve.Secp256k1, + Blockchain.Dogecoin to EllipticCurve.Secp256k1, + Blockchain.Ducatus to EllipticCurve.Secp256k1, + Blockchain.Ethereum to EllipticCurve.Secp256k1, + Blockchain.EthereumTestnet to EllipticCurve.Secp256k1, + Blockchain.EthereumClassic to EllipticCurve.Secp256k1, + Blockchain.EthereumClassicTestnet to EllipticCurve.Secp256k1, + Blockchain.Fantom to EllipticCurve.Secp256k1, + Blockchain.FantomTestnet to EllipticCurve.Secp256k1, + Blockchain.Litecoin to EllipticCurve.Secp256k1, + Blockchain.Near to EllipticCurve.Ed25519Slip0010, + Blockchain.NearTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Polkadot to EllipticCurve.Ed25519Slip0010, + Blockchain.PolkadotTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Kava to EllipticCurve.Secp256k1, + Blockchain.KavaTestnet to EllipticCurve.Secp256k1, + Blockchain.Kusama to EllipticCurve.Ed25519Slip0010, + Blockchain.Polygon to EllipticCurve.Secp256k1, + Blockchain.PolygonTestnet to EllipticCurve.Secp256k1, + Blockchain.RSK to EllipticCurve.Secp256k1, + Blockchain.Sei to EllipticCurve.Secp256k1, + Blockchain.SeiTestnet to EllipticCurve.Secp256k1, + Blockchain.Stellar to EllipticCurve.Ed25519Slip0010, + Blockchain.StellarTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Solana to EllipticCurve.Ed25519Slip0010, + Blockchain.SolanaTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Tezos to EllipticCurve.Ed25519Slip0010, + Blockchain.Tron to EllipticCurve.Secp256k1, + Blockchain.TronTestnet to EllipticCurve.Secp256k1, + Blockchain.XRP to EllipticCurve.Secp256k1, + Blockchain.Gnosis to EllipticCurve.Secp256k1, + Blockchain.Dash to EllipticCurve.Secp256k1, + Blockchain.Optimism to EllipticCurve.Secp256k1, + Blockchain.OptimismTestnet to EllipticCurve.Secp256k1, + Blockchain.Dischain to EllipticCurve.Secp256k1, + Blockchain.EthereumPow to EllipticCurve.Secp256k1, + Blockchain.EthereumPowTestnet to EllipticCurve.Secp256k1, + Blockchain.Kaspa to EllipticCurve.Secp256k1, + Blockchain.Telos to EllipticCurve.Secp256k1, + Blockchain.TelosTestnet to EllipticCurve.Secp256k1, + Blockchain.TON to EllipticCurve.Ed25519Slip0010, + Blockchain.TONTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Ravencoin to EllipticCurve.Secp256k1, + Blockchain.RavencoinTestnet to EllipticCurve.Secp256k1, + Blockchain.TerraV1 to EllipticCurve.Secp256k1, + Blockchain.TerraV2 to EllipticCurve.Secp256k1, + Blockchain.Cronos to EllipticCurve.Secp256k1, + Blockchain.AlephZero to EllipticCurve.Ed25519Slip0010, + Blockchain.AlephZeroTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.OctaSpace to EllipticCurve.Secp256k1, + Blockchain.OctaSpaceTestnet to EllipticCurve.Secp256k1, + Blockchain.Chia to EllipticCurve.Bls12381G2Aug, + Blockchain.ChiaTestnet to EllipticCurve.Bls12381G2Aug, + Blockchain.Decimal to EllipticCurve.Secp256k1, + Blockchain.DecimalTestnet to EllipticCurve.Secp256k1, + Blockchain.XDC to EllipticCurve.Secp256k1, + Blockchain.XDCTestnet to EllipticCurve.Secp256k1, + Blockchain.VeChain to EllipticCurve.Secp256k1, + Blockchain.VeChainTestnet to EllipticCurve.Secp256k1, + Blockchain.Aptos to EllipticCurve.Ed25519Slip0010, + Blockchain.AptosTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Playa3ull to EllipticCurve.Secp256k1, + Blockchain.Shibarium to EllipticCurve.Secp256k1, + Blockchain.ShibariumTestnet to EllipticCurve.Secp256k1, + Blockchain.Algorand to EllipticCurve.Ed25519Slip0010, + Blockchain.AlgorandTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Hedera to EllipticCurve.Ed25519Slip0010, + Blockchain.HederaTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Aurora to EllipticCurve.Secp256k1, + Blockchain.AuroraTestnet to EllipticCurve.Secp256k1, + Blockchain.Areon to EllipticCurve.Secp256k1, + Blockchain.AreonTestnet to EllipticCurve.Secp256k1, + Blockchain.PulseChain to EllipticCurve.Secp256k1, + Blockchain.PulseChainTestnet to EllipticCurve.Secp256k1, + Blockchain.ZkSyncEra to EllipticCurve.Secp256k1, + Blockchain.ZkSyncEraTestnet to EllipticCurve.Secp256k1, + Blockchain.Nexa to EllipticCurve.Secp256k1, + Blockchain.NexaTestnet to EllipticCurve.Secp256k1, + Blockchain.Moonbeam to EllipticCurve.Secp256k1, + Blockchain.MoonbeamTestnet to EllipticCurve.Secp256k1, + Blockchain.Manta to EllipticCurve.Secp256k1, + Blockchain.MantaTestnet to EllipticCurve.Secp256k1, + Blockchain.PolygonZkEVM to EllipticCurve.Secp256k1, + Blockchain.PolygonZkEVMTestnet to EllipticCurve.Secp256k1, + Blockchain.Radiant to EllipticCurve.Secp256k1, + Blockchain.Base to EllipticCurve.Secp256k1, + Blockchain.BaseTestnet to EllipticCurve.Secp256k1, + Blockchain.Moonriver to EllipticCurve.Secp256k1, + Blockchain.MoonriverTestnet to EllipticCurve.Secp256k1, + Blockchain.Mantle to EllipticCurve.Secp256k1, + Blockchain.MantleTestnet to EllipticCurve.Secp256k1, + Blockchain.Fact0rn to EllipticCurve.Secp256k1, + Blockchain.Flare to EllipticCurve.Secp256k1, + Blockchain.FlareTestnet to EllipticCurve.Secp256k1, + Blockchain.Taraxa to EllipticCurve.Secp256k1, + Blockchain.TaraxaTestnet to EllipticCurve.Secp256k1, + Blockchain.Koinos to EllipticCurve.Secp256k1, + Blockchain.KoinosTestnet to EllipticCurve.Secp256k1, + Blockchain.Joystream to EllipticCurve.Ed25519Slip0010, + Blockchain.Bittensor to EllipticCurve.Ed25519Slip0010, + Blockchain.Filecoin to EllipticCurve.Secp256k1, + Blockchain.Blast to EllipticCurve.Secp256k1, + Blockchain.BlastTestnet to EllipticCurve.Secp256k1, + Blockchain.Cyber to EllipticCurve.Secp256k1, + Blockchain.CyberTestnet to EllipticCurve.Secp256k1, + Blockchain.InternetComputer to EllipticCurve.Secp256k1, + Blockchain.Sui to EllipticCurve.Ed25519Slip0010, + Blockchain.SuiTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.EnergyWebChain to EllipticCurve.Secp256k1, + Blockchain.EnergyWebChainTestnet to EllipticCurve.Secp256k1, + Blockchain.EnergyWebX to EllipticCurve.Ed25519Slip0010, + Blockchain.EnergyWebXTestnet to EllipticCurve.Ed25519Slip0010, + Blockchain.Casper to EllipticCurve.Secp256k1, + Blockchain.CasperTestnet to EllipticCurve.Secp256k1, + Blockchain.Core to EllipticCurve.Secp256k1, + Blockchain.CoreTestnet to EllipticCurve.Secp256k1, + Blockchain.Xodex to EllipticCurve.Secp256k1, + Blockchain.Canxium to EllipticCurve.Secp256k1, + Blockchain.Chiliz to EllipticCurve.Secp256k1, + Blockchain.ChilizTestnet to EllipticCurve.Secp256k1, + Blockchain.Clore to EllipticCurve.Secp256k1, + Blockchain.VanarChain to EllipticCurve.Secp256k1, + Blockchain.VanarChainTestnet to EllipticCurve.Secp256k1, + Blockchain.OdysseyChain to EllipticCurve.Secp256k1, + Blockchain.OdysseyChainTestnet to EllipticCurve.Secp256k1, + Blockchain.Bitrock to EllipticCurve.Secp256k1, + Blockchain.BitrockTestnet to EllipticCurve.Secp256k1, + ) + + @Test + fun blockchainCurveMappingTest() { + Blockchain.entries.forEach { + val resultCurve = wallet2CardConfig.primaryCurve(it) + assertEquals(expectedMapping[it], resultCurve) + } + } +} \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index bf25c0f8b6..717c52e49c 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -5,7 +5,10 @@ plugins { } dependencies { + api(projects.domain.visa.models) + implementation(deps.tangem.card.core) implementation(deps.moshi.kotlin) + implementation(deps.moshi.adapters) implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 6a3b017398..98954e2c5c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass import com.tangem.common.card.WalletData import com.tangem.common.extensions.ByteArrayKey import com.tangem.domain.models.scan.serialization.ScanResponseAsStringSerializer +import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.ExtendedPublicKeysMap import kotlinx.serialization.Serializable @@ -20,6 +21,7 @@ data class ScanResponse( @Json(name = "productType") val productType: ProductType, @Json(name = "walletData") val walletData: WalletData?, @Json(name = "secondTwinPublicKey") val secondTwinPublicKey: String? = null, + @Json(name = "visaCardActivationStatus") val visaCardActivationStatus: VisaCardActivationStatus? = null, @Json(name = "derivedKeys") val derivedKeys: Map = mapOf(), @Json(name = "primaryCard") val primaryCard: PrimaryCard? = null, ) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt index b3cb3d847e..7f22e52275 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.TangemSdkAdapter import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaCardActivationStatus import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -22,6 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaCardActivationStatus.jsonAdapter) .add(KotlinJsonAdapterFactory()) .build() diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt index 2891d6072d..81a3c174d2 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -21,7 +21,7 @@ sealed interface OnrampProviderWithQuote { val availablePaymentMethods: List, ) : Unavailable - data class Error( + data class AmountError( override val provider: OnrampProvider, val quoteError: OnrampQuote.AmountError, ) : Unavailable diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt index 1d6c6700d6..be975c49dd 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -40,7 +40,7 @@ class GetOnrampProviderWithQuoteUseCase( toAmount = matchedQuote.toAmount, fromAmount = matchedQuote.fromAmount, ) - is OnrampQuote.AmountError -> Unavailable.Error( + is OnrampQuote.AmountError -> Unavailable.AmountError( provider = matchedQuote.provider, quoteError = matchedQuote, ) diff --git a/domain/promo/.gitignore b/domain/promo/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/promo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/promo/build.gradle.kts b/domain/promo/build.gradle.kts new file mode 100644 index 0000000000..cefe592d97 --- /dev/null +++ b/domain/promo/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(projects.domain.promo.models) + implementation(projects.domain.wallets.models) + + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) +} \ No newline at end of file diff --git a/domain/promo/models/.gitignore b/domain/promo/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/promo/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/promo/models/build.gradle.kts b/domain/promo/models/build.gradle.kts new file mode 100644 index 0000000000..fe9e75a251 --- /dev/null +++ b/domain/promo/models/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt similarity index 92% rename from domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt rename to domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt index 2608085143..42ecbd01d8 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt +++ b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.promo +package com.tangem.domain.promo.models import org.joda.time.DateTime diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt new file mode 100644 index 0000000000..de2c366987 --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.promo + +import com.tangem.domain.promo.models.PromoBanner +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface PromoRepository { + + suspend fun getChangellyPromoBanner(): PromoBanner? + + suspend fun getOkxPromoBanner(): PromoBanner? + + suspend fun getRingPromoBanner(): PromoBanner? + + fun isReadyToShowWalletSwapPromo(): Flow + + fun isReadyToShowTokenSwapPromo(): Flow + + suspend fun setNeverToShowWalletSwapPromo() + + suspend fun setNeverToShowTokenSwapPromo() + + fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow + + suspend fun setNeverToShowRingPromo() + + fun isReadyToShowSwapStories(): Flow + + suspend fun isReadyToShowSwapStoriesSync(): Boolean + + suspend fun setNeverToShowSwapStories() +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt new file mode 100644 index 0000000000..78ad8931ef --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.promo + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +class ShouldShowRingPromoUseCase(private val promoRepository: PromoRepository) { + + operator fun invoke(userWalletId: UserWalletId): Flow { + return promoRepository.isReadyToShowRingPromo(userWalletId) + } + + suspend fun neverToShow() = promoRepository.setNeverToShowRingPromo() +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt new file mode 100644 index 0000000000..9bd3a02d7f --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.promo + +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoTokenUseCase(private val promoRepository: PromoRepository) { + + operator fun invoke(): Flow = promoRepository.isReadyToShowTokenSwapPromo() + + suspend fun neverToShow() = promoRepository.setNeverToShowTokenSwapPromo() +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt new file mode 100644 index 0000000000..5ed815ec9c --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.promo + +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoWalletUseCase(private val promoRepository: PromoRepository) { + + operator fun invoke(): Flow = promoRepository.isReadyToShowWalletSwapPromo() + + suspend fun neverToShow() = promoRepository.setNeverToShowWalletSwapPromo() +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapStoriesUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapStoriesUseCase.kt new file mode 100644 index 0000000000..35d46bbf27 --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapStoriesUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.promo + +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapStoriesUseCase(private val promoRepository: PromoRepository) { + operator fun invoke(): Flow = promoRepository.isReadyToShowSwapStories() + suspend fun invokeSync(): Boolean = promoRepository.isReadyToShowSwapStoriesSync() + + suspend fun neverToShow() = promoRepository.setNeverToShowSwapStories() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt deleted file mode 100644 index 67d9174e78..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.PromoSettingsRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -class ShouldShowRingPromoUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - - operator fun invoke(userWalletId: UserWalletId): Flow { - return promoSettingsRepository.isReadyToShowRingPromo(userWalletId) - } - - suspend fun neverToShow() = promoSettingsRepository.setNeverToShowRingPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt deleted file mode 100644 index 8ede57bcf9..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.PromoSettingsRepository -import kotlinx.coroutines.flow.Flow - -class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - - operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowTokenSwapPromo() - - suspend fun neverToShow() = promoSettingsRepository.setNeverToShowTokenSwapPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt deleted file mode 100644 index 233277f71f..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.PromoSettingsRepository -import kotlinx.coroutines.flow.Flow - -class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - - operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletSwapPromo() - - suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletSwapPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt deleted file mode 100644 index b68a740e09..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.settings.repositories - -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -interface PromoSettingsRepository { - fun isReadyToShowWalletSwapPromo(): Flow - - fun isReadyToShowTokenSwapPromo(): Flow - - suspend fun setNeverToShowWalletSwapPromo() - - suspend fun setNeverToShowTokenSwapPromo() - - fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow - - suspend fun setNeverToShowRingPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 22f8fec52b..8670f0ce4f 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.settings.repositories import com.tangem.domain.settings.usercountry.models.UserCountry +import kotlinx.coroutines.flow.StateFlow @Suppress("TooManyFunctions") interface SettingsRepository { @@ -39,6 +40,8 @@ interface SettingsRepository { suspend fun getUserCountryCodeSync(): UserCountry? + fun getUserCountryCode(): StateFlow + suspend fun fetchUserCountryCode() suspend fun setGoogleServicesAvailability(value: Boolean) diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt index 01768f5a6c..cabd6677a9 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/usercountry/GetUserCountryUseCase.kt @@ -7,6 +7,8 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Get user country code use case @@ -19,7 +21,17 @@ class GetUserCountryUseCase( private val settingsRepository: SettingsRepository, ) { - suspend operator fun invoke(): Either { + operator fun invoke(): Flow> { + return settingsRepository.getUserCountryCode().map { userCountryCode -> + either { + ensureNotNull(userCountryCode) { UserCountryError.NotSetup } + + userCountryCode + } + } + } + + suspend fun invokeSync(): Either { return either { val userCountryCode = catch( block = { settingsRepository.getUserCountryCodeSync() }, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index 50eaec64e5..69ba4f794b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam.Key.CURRENCY import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM @@ -16,7 +15,7 @@ class TokenOnrampAnalyticsEvent( params = mapOf( TOKEN_PARAM to tokenSymbol, PROVIDER to provider, - CURRENCY to fiatCurrency, + "Currency Type" to fiatCurrency, ), ) @@ -27,7 +26,7 @@ class TokenOnrampAnalyticsEvent( TOKEN_PARAM to tokenSymbol, STATUS to status, PROVIDER to provider, - CURRENCY to fiatCurrency, + "Currency Type" to fiatCurrency, ), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index fe343236b9..1d4b2cc1d4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -192,44 +192,18 @@ class GetCryptoCurrencyActionsUseCase( ) } - // sell - val sellSupportedByService = rampManager.isSellSupportedByService(cryptoCurrency) - val sendAvailable = sendUnavailabilityReason is ScenarioUnavailabilityReason.None - - when { - sellSupportedByService && sendAvailable -> { + // region sell + rampManager.availableForSell( + userWalletId = userWallet.walletId, + status = cryptoCurrencyStatus, + ) + .onRight { activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) } - sellSupportedByService && !sendAvailable -> { - (sendUnavailabilityReason as? ScenarioUnavailabilityReason.EmptyBalance)?.let { - disabledList.add( - TokenActionsState.ActionState.Sell( - unavailabilityReason = it.copy( - withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, - ), - ), - ) - } - (sendUnavailabilityReason as? ScenarioUnavailabilityReason.PendingTransaction)?.let { - disabledList.add( - TokenActionsState.ActionState.Sell( - unavailabilityReason = it.copy( - withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, - ), - ), - ) - } + .onLeft { reason -> + disabledList.add(TokenActionsState.ActionState.Sell(reason)) } - else -> { - disabledList.add( - TokenActionsState.ActionState.Sell( - unavailabilityReason = ScenarioUnavailabilityReason.NotSupportedBySellService( - cryptoCurrency.name, - ), - ), - ) - } - } + // endregion // hide activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index e6975190ee..5be9720ad4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,37 +1,30 @@ package com.tangem.domain.tokens -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.model.warnings.KaspaWarnings import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import java.math.BigDecimal -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") class GetCurrencyWarningsUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val swapRepository: SwapRepository, - private val rampStateManager: RampStateManager, private val stakingRepository: StakingRepository, - private val promoRepository: PromoRepository, - private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val dispatchers: CoroutineDispatcherProvider, private val currencyChecksRepository: CurrencyChecksRepository, ) { @@ -78,75 +71,6 @@ class GetCurrencyWarningsUseCase( }.flowOn(dispatchers.io) } - // disabled for now, don't use it directly in combine() to avoid blocking other notifications - @Suppress("UnusedPrivateMember") - private suspend fun getSwapPromoNotificationWarning( - operations: CurrenciesStatusesOperations, - userWalletId: UserWalletId, - currencyStatus: CryptoCurrencyStatus, - ): Flow { - val currency = currencyStatus.currency - val cryptoStatuses = operations.getCurrenciesStatusesSync() - val promoBanner = promoRepository.getOkxPromoBanner() - return combine( - flow = showSwapPromoTokenUseCase() - .conflate() - .distinctUntilChanged(), - flow2 = flowOf(rampStateManager.availableForSwap(userWalletId, currency)) - .conflate() - .distinctUntilChanged(), - ) { shouldShowSwapPromo, isExchangeable -> - promoBanner ?: return@combine null - val showPromoBanner = promoBanner.isActive && shouldShowSwapPromo - if (showPromoBanner && isExchangeable && currencyStatus.value !is CryptoCurrencyStatus.Unreachable) { - cryptoStatuses.fold( - ifLeft = { null }, - ifRight = { cryptoCurrencyStatuses -> - val pairs = runCatching(dispatchers.io) { - swapRepository.getPairsOnly( - LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - cryptoCurrencyStatuses.map { it.currency }, - ) - }.getOrNull()?.pairs ?: emptyList() - - val filteredCurrencies = cryptoCurrencyStatuses.filterNot { - it.currency.id == currency.id - } - val currencyPairs = pairs.filter { - it.from.network == currency.network.backendId || - it.to.network == currency.network.backendId - } - val showPromo = currencyPairs.any { pair -> - val availablePair = if (currencyStatus.value.amount.isNullOrZero()) { - filteredCurrencies.filterNot { it.value.amount.isNullOrZero() } - } else { - filteredCurrencies - } - availablePair - .any { - it.currency.network.backendId == pair.to.network || - it.currency.network.backendId == pair.from.network - } - } - if (showPromo) { - CryptoCurrencyWarning.SwapPromo( - startDateTime = promoBanner.bannerState.timeline.start, - endDateTime = promoBanner.bannerState.timeline.end, - ) - } else { - null - } - }, - ) - } else { - null - } - } - } - @Suppress("LongParameterList") private suspend fun getCoinRelatedWarnings( userWalletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt deleted file mode 100644 index 39457c1113..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.domain.promo.PromoBanner - -interface PromoRepository { - - suspend fun getChangellyPromoBanner(): PromoBanner? - - suspend fun getOkxPromoBanner(): PromoBanner? - - suspend fun getRingPromoBanner(): PromoBanner? -} \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 8032c2e995..96fb032e84 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -9,6 +9,8 @@ android { } dependencies { + /** Domain models */ + api(projects.domain.visa.models) /** Project - Domain */ implementation(projects.core.utils) @@ -20,4 +22,6 @@ dependencies { /** Libs - Other */ implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/domain/visa/models/.gitignore b/domain/visa/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/visa/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/visa/models/build.gradle.kts b/domain/visa/models/build.gradle.kts new file mode 100644 index 0000000000..96a8bc4639 --- /dev/null +++ b/domain/visa/models/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.moshi.kotlin) + implementation(deps.moshi.adapters) + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt new file mode 100644 index 0000000000..a5924d0da4 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.visa.model + +data class ActivationOrder( + val hash: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt new file mode 100644 index 0000000000..9c16d85bfc --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class SignedActivationOrder( + val activationOrder: ActivationOrder, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt new file mode 100644 index 0000000000..11685b3866 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.visa.model + +enum class VisaActivationError(val message: String) { + BlockedForActivation("Card is blocked for activation"), + InvalidActivationState("Invalid activation state"), + WrongCard("Wrong card tapped"), + MissingRootOTP("Missing root OTP"), +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt new file mode 100644 index 0000000000..5749e737d8 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class VisaActivationInput( + @Json(name = "cardId") val cardId: String, + @Json(name = "cardPublicKey") val cardPublicKey: ByteArray, + @Json(name = "isAccessCodeSet") val isAccessCodeSet: Boolean, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VisaActivationInput + + if (cardId != other.cardId) return false + if (!cardPublicKey.contentEquals(other.cardPublicKey)) return false + if (isAccessCodeSet != other.isAccessCodeSet) return false + + return true + } + + override fun hashCode(): Int { + var result = cardId.hashCode() + result = 31 * result + cardPublicKey.contentHashCode() + result = 31 * result + isAccessCodeSet.hashCode() + return result + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt new file mode 100644 index 0000000000..3b64c35fde --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.visa.model + +import kotlinx.serialization.Serializable + +@Serializable +enum class VisaActivationRemoteState { + CardWalletSignatureRequired, + CustomerWalletSignatureRequired, + PaymentAccountDeploying, + WaitingPinCode, + WaitingForActivationFinishing, + Activated, + BlockedForActivation, +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt new file mode 100644 index 0000000000..536b7207dc --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.visa.model + +sealed class VisaAuthChallenge { + + abstract val session: VisaAuthSession + + data class Card( + val challenge: String, + override val session: VisaAuthSession, + ) : VisaAuthChallenge() + + data class Wallet( + val challenge: String, + override val session: VisaAuthSession, + ) : VisaAuthChallenge() +} + +fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge { + return VisaAuthSignedChallenge.ByCardPublicKey( + challenge = this, + signature = signedChallenge, + salt = salt, + ) +} + +fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge { + return VisaAuthSignedChallenge.ByWallet( + challenge = this, + signature = signedChallenge, + ) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt new file mode 100644 index 0000000000..67437218f5 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.visa.model + +@JvmInline +value class VisaAuthSession(val sessionId: String) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt new file mode 100644 index 0000000000..a16916a887 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.visa.model + +sealed class VisaAuthSignedChallenge { + + data class ByCardPublicKey( + val challenge: VisaAuthChallenge.Card, + val signature: String, + val salt: String, + ) : VisaAuthSignedChallenge() + + data class ByWallet( + val challenge: VisaAuthChallenge.Wallet, + val signature: String, + ) : VisaAuthSignedChallenge() +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt new file mode 100644 index 0000000000..0341e9ad8a --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class VisaAuthTokens( + @Json(name = "accessToken") val accessToken: String, + @Json(name = "refreshToken") val refreshToken: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt new file mode 100644 index 0000000000..79cc4c9a95 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaCardActivationResponse( + val signedActivationOrder: SignedActivationOrder, + val rootOTP: VisaRootOTP, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt new file mode 100644 index 0000000000..7e1a10a1c8 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import kotlinx.serialization.Serializable + +@Serializable +sealed class VisaCardActivationStatus { + + @Serializable + data class Activated( + @Json(name = "visaAuthTokens") val visaAuthTokens: VisaAuthTokens, + ) : VisaCardActivationStatus() + + @Serializable + data class ActivationStarted( + @Json(name = "activationInput") val activationInput: VisaActivationInput, + @Json(name = "authTokens") val authTokens: VisaAuthTokens, + @Json(name = "remoteState") val remoteState: VisaActivationRemoteState, + ) : VisaCardActivationStatus() + + @Serializable + data class NotStartedActivation( + @Json(name = "activationInput") val activationInput: VisaActivationInput, + ) : VisaCardActivationStatus() + + @Serializable + data object Blocked : VisaCardActivationStatus() + + companion object { + val jsonAdapter: PolymorphicJsonAdapterFactory + get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") + .withSubtype(Activated::class.java, "Activated") + .withSubtype(ActivationStarted::class.java, "ActivationStarted") + .withSubtype(NotStartedActivation::class.java, "NotStartedActivation") + .withDefaultValue(Blocked) + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt new file mode 100644 index 0000000000..310ee01021 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.visa.model + +@JvmInline +value class VisaRootOTP(val value: String) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt new file mode 100644 index 0000000000..8127f921c6 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.visa.repository + +import com.tangem.domain.visa.model.ActivationOrder +import com.tangem.domain.visa.model.VisaActivationRemoteState + +interface VisaActivationRepository { + + suspend fun getActivationRemoteState(): VisaActivationRemoteState + + suspend fun getActivationOrderToSign(): ActivationOrder +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt new file mode 100644 index 0000000000..a280b47b2c --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.visa.repository + +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSignedChallenge +import com.tangem.domain.visa.model.VisaAuthTokens + +interface VisaAuthRepository { + + suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card + + suspend fun getCustomerWalletAuthChallenge(cardId: String, walletPublicKey: String): VisaAuthChallenge.Wallet + + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens +} \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index edd1284284..574898644a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,14 +21,22 @@ platform :android do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services.json", "../app") end - desc "Runs all the tests" - lane :test do + desc "Run detekt" + lane :detekt do + FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") gradle(task: "detekt") + end + + desc "Run tests" + lane :test do + FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") gradle(task: "testDebugUnitTest") end desc "Build a signed release APK" - lane :release do |options| + lane :release do |options| # TODO: remove after fully moving from old CI gradle( task: "clean assemble", build_type: "Release", @@ -41,8 +49,9 @@ platform :android do end desc "Build external and release APKs" - lane :build do |options| + lane :build do |options| # TODO: remove after fully moving from old CI gradle(task: 'clean') + gradle( task: "bundle", build_type: "Release", @@ -69,29 +78,76 @@ platform :android do ) end + desc "Build release AAB and APK" + lane :buildRelease do |options| + FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") + + gradle( + task: "bundle", + build_type: "Release", + properties: { + 'versionCode' => options[:versionCode], + 'versionName' => options[:versionName], + "android.injected.signing.store.file" => options[:keystore], + "android.injected.signing.store.password" => options[:store_password], + "android.injected.signing.key.alias" => options[:key_alias], + "android.injected.signing.key.password" => options[:key_password], + } + ) + gradle( + task: "assemble", + build_type: "Release", + properties: { + 'versionCode' => options[:versionCode], + 'versionName' => options[:versionName], + "android.injected.signing.store.file" => options[:keystore], + "android.injected.signing.store.password" => options[:store_password], + "android.injected.signing.key.alias" => options[:key_alias], + "android.injected.signing.key.password" => options[:key_password], + } + ) + end + desc "Submit a new Beta Build to Firebase App Distribution" - lane :beta do |options| + lane :beta do |options| # TODO: remove after fully moving from old CI firebase_app_distribution( app: options[:app_id], apk_path: options[:apk_path], groups: options[:groups]) end - desc "Publish internal and external builds to Firebase App Distribution" - lane :publishToFirebase do |options| + + desc "Build internal APK Firebase App Distribution" + lane :buildInternal do |options| + FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") + gradle( - task: "clean assemble", + task: "assemble", build_type: "Internal", properties: { - 'versionCode' => ENV['versionCode'], - 'versionName' => ENV['versionName'], + 'versionCode' => ENV['version_code'], + 'versionName' => ENV['version_name'], } - ) - firebase_app_distribution( + ) + end + + desc "Publish internal build to Firebase App Distribution" + lane :publishToFirebase do |options| + uploaded_release = firebase_app_distribution( + firebase_cli_token: ENV['firebase_cli_token'], app: ENV['app_id_internal'], apk_path: ENV['apk_path_internal'], groups: ENV['groups'], - release_notes: ENV['releaseNotes'] + release_notes: ENV['release_notes'], ) + release_url = uploaded_release[:testingUri] + + # Applying base64 encoding twice to prevent GA bug "Skip output 'output' since it may contain secret." + # See https://github.com/orgs/community/discussions/13082#discussioncomment-6776428 for details + encoded_release_url = Base64.strict_encode64(Base64.strict_encode64(release_url)) + sh("echo 'encoded_release_url=#{encoded_release_url}' >> /workspace/github_output.txt") end + end diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 98302fc6f7..0c2faf9735 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -18,14 +18,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockItem -import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags @@ -48,12 +46,6 @@ internal fun DetailsScreen( Scaffold( modifier = modifier, containerColor = backgroundColor, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - hostState = LocalSnackbarHostState.current, - ) - }, topBar = { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), @@ -86,7 +78,7 @@ private fun Content( item { Text( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - text = stringResource(R.string.details_title), + text = stringResourceSafe(R.string.details_title), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) @@ -127,7 +119,9 @@ private fun Block( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, ) { - val itemModifier = Modifier.fillMaxWidth().testTag(TestTags.DETAILS_SCREEN_ITEM) + val itemModifier = Modifier + .fillMaxWidth() + .testTag(TestTags.DETAILS_SCREEN_ITEM) when (model) { is DetailsItemUM.Basic -> { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt index b2e05c0504..e9c6048867 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.impl.R @@ -37,12 +37,12 @@ internal fun WalletConnectBlock(onClick: () -> Unit, modifier: Modifier = Modifi verticalArrangement = Arrangement.SpaceAround, ) { Text( - text = stringResource(id = R.string.wallet_connect_title), + text = stringResourceSafe(id = R.string.wallet_connect_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) Text( - text = stringResource(id = R.string.wallet_connect_subtitle), + text = stringResourceSafe(id = R.string.wallet_connect_subtitle), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index a427f0fc29..fd621dbe14 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -1,6 +1,5 @@ package com.tangem.features.details.utils -import androidx.compose.ui.res.stringResource import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.ensureNotNull @@ -12,12 +11,11 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.popTo import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse @@ -105,12 +103,9 @@ internal class UserWalletSaver @Inject constructor( private fun selectUserWallet() { messageSender.send( - message = ContentMessage { onDismiss -> - SimpleOkDialog( - message = stringResource(id = R.string.user_wallet_list_error_wallet_already_saved), - onDismissDialog = onDismiss, - ) - }, + message = DialogMessage( + message = resourceReference(R.string.user_wallet_list_error_wallet_already_saved), + ), ) } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 7ffb017cfa..a1b849f65f 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.ButtonColors import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -16,7 +17,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -29,8 +29,8 @@ import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -153,12 +153,12 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { rememberPermissionState(permission = permission).status.isGranted } ?: true PrimaryButton( - text = stringResource(id = R.string.common_accept), + text = stringResourceSafe(id = R.string.common_accept), onClick = { onAccept(!isPermissionGranted) }, - colors = TangemButtonColors( - backgroundColor = TangemColorPalette.Light4, + colors = ButtonColors( + containerColor = TangemColorPalette.Light4, contentColor = TangemColorPalette.Dark6, - disabledBackgroundColor = TangemColorPalette.Light4, + disabledContainerColor = TangemColorPalette.Light4, disabledContentColor = TangemColorPalette.Dark6, ), modifier = Modifier diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 563c100c5e..10cf6ddcc7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -62,7 +62,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( override fun BottomSheet() { val config = remember { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = initialConfiguration.step.toContentModel(::popBack), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index cfac082152..d11eaaaf24 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -29,7 +29,7 @@ internal class PreviewAddCustomTokenComponent( override fun BottomSheet() { val config by previewConfig.collectAsStateWithLifecycle() val bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = config.step.toContentModel(::dismiss), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 1e4f48feec..ad5db50c72 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,16 +1,14 @@ package com.tangem.features.managetokens.model -import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException @@ -229,12 +227,9 @@ internal class CustomTokenFormModel @Inject constructor( } private fun showErrorDialog() { - val dialog = ContentMessage { onDismiss -> - SimpleOkDialog( - message = stringResource(R.string.common_unknown_error), - onDismissDialog = onDismiss, - ) - } + val dialog = DialogMessage( + message = resourceReference(R.string.common_unknown_error), + ) messageSender.send(dialog) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 1b01e1e8bf..93e9697e4b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -33,6 +32,7 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent @@ -89,7 +89,7 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier .align(Alignment.BottomCenter) .padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight) .fillMaxWidth(), - text = stringResource(id = R.string.custom_token_add_token), + text = stringResourceSafe(id = R.string.custom_token_add_token), colors = TangemButtonsDefaults.primaryButtonColors, enabled = model.canAddToken, showProgress = model.isValidating, @@ -162,7 +162,7 @@ private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier) { model.error != null -> { TangemTheme.colors.text.warning } - model.value.isNotBlank() || model.isFocused -> { + model.isEnabled || model.value.isNotBlank() || model.isFocused -> { TangemTheme.colors.text.tertiary } else -> { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 2fed1003f0..1d76d45e8a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -28,6 +27,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.tokens.model.Network @@ -137,7 +137,7 @@ private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = modifier = Modifier .padding(top = TangemTheme.dimens.spacing12) .padding(horizontal = TangemTheme.dimens.spacing12), - text = stringResource(R.string.add_custom_token_choose_network), + text = stringResourceSafe(R.string.add_custom_token_choose_network), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -189,13 +189,14 @@ private fun CustomDerivationButton( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = stringResource(id = R.string.custom_token_custom_derivation), + text = stringResourceSafe(id = R.string.custom_token_custom_derivation), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, ) Text( modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = enteredDerivationPath ?: stringResource(id = R.string.custom_token_custom_derivation_title), + text = enteredDerivationPath + ?: stringResourceSafe(id = R.string.custom_token_custom_derivation_title), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index f7643e76c1..e35c8871c7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -51,13 +50,12 @@ import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.ChainRowContainer import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM -import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @@ -100,12 +98,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi state = state, ) }, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - hostState = LocalSnackbarHostState.current, - ) - }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { if (state is ManageTokensUM.ManageContent) { @@ -165,7 +157,7 @@ private fun SaveChangesButton( label = "save_button_visibility", ) { TangemButton( - text = stringResource(id = R.string.common_save), + text = stringResourceSafe(id = R.string.common_save), icon = if (showIcon) { TangemButtonIconPosition.End(R.drawable.ic_tangem_24) } else { @@ -277,7 +269,7 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) { contentAlignment = Alignment.Center, ) { Text( - text = stringResource(R.string.markets_search_token_no_result_title), + text = stringResourceSafe(R.string.markets_search_token_no_result_title), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/OnboardingManageTokensContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/OnboardingManageTokensContent.kt index d40b7fb3e2..1d764d21a2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/OnboardingManageTokensContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/OnboardingManageTokensContent.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -18,9 +17,8 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.fields.SearchBar -import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @@ -53,14 +51,6 @@ internal fun OnboardingManageTokensContent(state: OnboardingManageTokensUM, modi state = state, ) }, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing16) - .navigationBarsPadding(), - hostState = LocalSnackbarHostState.current, - ) - }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { FloatingActionButton( @@ -102,7 +92,7 @@ private fun FloatingActionButton(config: OnboardingManageTokensUM.ActionButtonCo is OnboardingManageTokensUM.ActionButtonConfig.Continue -> ContinueButton(config = config, modifier = modifier) is OnboardingManageTokensUM.ActionButtonConfig.Later -> SecondaryButton( modifier = modifier, - text = stringResource(id = R.string.common_later), + text = stringResourceSafe(id = R.string.common_later), showProgress = config.showProgress, onClick = config.onClick, ) @@ -117,7 +107,7 @@ private fun ContinueButton( if (config.showTangemIcon) { PrimaryButtonIconEnd( modifier = modifier, - text = stringResource(id = R.string.common_continue), + text = stringResourceSafe(id = R.string.common_continue), iconResId = R.drawable.ic_tangem_24, showProgress = config.showProgress, onClick = config.onClick, @@ -125,7 +115,7 @@ private fun ContinueButton( } else { PrimaryButton( modifier = modifier, - text = stringResource(id = R.string.common_continue), + text = stringResourceSafe(id = R.string.common_continue), showProgress = config.showProgress, onClick = config.onClick, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt index 3b95cd9e8c..6bd84f1f08 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt @@ -4,8 +4,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R @@ -13,7 +13,7 @@ import com.tangem.features.managetokens.impl.R internal fun AddCustomTokenDescription(modifier: Modifier = Modifier) { Text( modifier = modifier.fillMaxWidth(fraction = 0.7f), - text = stringResource(id = R.string.custom_token_subtitle), + text = stringResourceSafe(id = R.string.custom_token_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt deleted file mode 100644 index 659b007c29..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.managetokens.ui.dialog - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.features.managetokens.impl.R - -@Composable -internal fun CurrencyUnsupportedDialog(title: TextReference, message: TextReference, onDismiss: () -> Unit) { - BasicDialog( - title = title.resolveReference(), - message = message.resolveReference(), - confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt index be41894576..d9cb84c75d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -12,6 +11,7 @@ import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenDerivationInputComponent @@ -23,21 +23,21 @@ internal fun CustomDerivationInputDialog(model: CustomDerivationInputUM, onDismi val value by rememberUpdatedState(newValue = model.value) TextInputDialog( - title = stringResource(id = R.string.custom_token_custom_derivation_title), + title = stringResourceSafe(id = R.string.custom_token_custom_derivation_title), fieldValue = value, confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, onValueChange = model.updateValue, textFieldParams = AdditionalTextInputDialogUM( - label = model.error?.resolveReference() ?: stringResource(id = R.string.custom_token_derivation_path), + label = model.error?.resolveReference() ?: stringResourceSafe(id = R.string.custom_token_derivation_path), isError = model.error != null, ), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt deleted file mode 100644 index 848d654e08..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.features.managetokens.ui.dialog - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.features.managetokens.impl.R - -@Composable -internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) { - BasicDialog( - title = stringResource( - R.string.token_details_unable_hide_alert_title, - currency.name, - ), - message = stringResource( - R.string.token_details_unable_hide_alert_message, - currency.name, - currency.symbol, - network.name, - ), - confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt deleted file mode 100644 index 46b9cc6a39..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.managetokens.ui.dialog - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.features.managetokens.impl.R - -@Composable -internal fun HideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit, onDismiss: () -> Unit) { - BasicDialog( - title = stringResource( - R.string.token_details_hide_alert_title, - currency.name, - ), - message = stringResource(R.string.token_details_hide_alert_message), - confirmButton = DialogButtonUM( - title = stringResource(R.string.token_details_hide_alert_hide), - warning = true, - onClick = onConfirm, - ), - dismissButton = DialogButtonUM( - title = stringResource(R.string.common_cancel), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 8ed4841f87..b3c1d8d3d7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -5,7 +5,8 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency @@ -13,9 +14,6 @@ import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.impl.R -import com.tangem.features.managetokens.ui.dialog.CurrencyUnsupportedDialog -import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning -import com.tangem.features.managetokens.ui.dialog.HideTokenWarning import com.tangem.features.managetokens.utils.mapper.toUiModel import com.tangem.features.managetokens.utils.ui.toggleExpanded import com.tangem.features.managetokens.utils.ui.update @@ -152,7 +150,7 @@ internal class ManageTokensUiManager( private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) { if (source is ManagedCryptoCurrency.SourceNetwork.Default) { - clipboardManager.setText(text = source.contractAddress) + clipboardManager.setText(text = source.contractAddress, isSensitive = false) showSnackbarMessage(resourceReference(R.string.contract_address_copied_message)) } } @@ -195,26 +193,23 @@ internal class ManageTokensUiManager( } private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = ContentMessage { onDismiss -> - CurrencyUnsupportedDialog( - title = resourceReference(R.string.common_warning), - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - onDismiss = onDismiss, - ) - } + val message = DialogMessage( + title = resourceReference(R.string.common_warning), + message = when (unsupportedState) { + is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + }, + ) messageSender.send(message) } @@ -243,27 +238,40 @@ internal class ManageTokensUiManager( } private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) { - val message = ContentMessage { onDismiss -> - HasLinkedTokensWarning( - currency = currency, - network = network, - onDismiss = onDismiss, - ) - } + val message = DialogMessage( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currency.name), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList( + currency.name, + currency.symbol, + network.name, + ), + ), + ) messageSender.send(message) } private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) { - val message = ContentMessage { onDismiss -> - HideTokenWarning( - currency = currency, - onConfirm = { - onConfirm() - onDismiss() - }, - onDismiss = onDismiss, - ) - } + val message = DialogMessage( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currency.name), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.token_details_hide_alert_hide), + warning = true, + onClick = onConfirm, + ) + }, + secondActionBuilder = { cancelAction() }, + ) + messageSender.send(message) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 5121d721b4..c6c2a86b49 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -205,7 +205,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( body = MarketsTokenDetailsUM.Body.Loading, triggerPriceChange = consumedEvent(), bottomSheetConfig = TangemBottomSheetConfig( - isShow = false, + isShown = false, onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), @@ -510,7 +510,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( state.update { stateToUpdate -> stateToUpdate.copy( bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy( - isShow = true, + isShown = true, onDismissRequest = ::hideBottomSheet, content = content, ), @@ -521,7 +521,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private fun hideBottomSheet() { state.update { stateToUpdate -> stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShow = false), + bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false), ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 2aa2188b32..6b22d2f6e3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -336,7 +336,7 @@ private fun Preview() { onSelectedIntervalChange = { }, body = MarketsTokenDetailsUM.Body.Loading, bottomSheetConfig = TangemBottomSheetConfig( - isShow = false, + isShown = false, onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt index 987689d400..76a4172080 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -15,6 +14,7 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer @@ -34,7 +34,12 @@ internal fun Description( append(description.resolveReference()) } withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append(" " + stringResource(R.string.common_read_more).replace(' ', StringsSigns.NON_BREAKING_SPACE)) + append( + " " + stringResourceSafe(R.string.common_read_more).replace( + ' ', + StringsSigns.NON_BREAKING_SPACE, + ), + ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt index 8a0040876c..4c7bd46d6c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -27,10 +26,7 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent @@ -92,7 +88,7 @@ internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) { TangemTopAppBar( - title = stringResource(id = textResId), + title = stringResourceSafe(id = textResId), startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick), ) } @@ -128,7 +124,7 @@ private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(id = content.message), + text = stringResourceSafe(id = content.message), color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, style = TangemTheme.typography.caption1, @@ -156,7 +152,7 @@ private fun Preview_ExchangesBottomSheet( config = TangemBottomSheetConfig( onDismissRequest = {}, content = content, - isShow = true, + isShown = true, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index 2b02fdb118..5825ba908b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -16,6 +15,7 @@ import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.chip.Chip import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer @@ -31,7 +31,7 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { contentHorizontalPadding = 0.dp, title = { Text( - text = stringResource(id = R.string.markets_token_details_links), + text = stringResourceSafe(id = R.string.markets_token_details_links), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, maxLines = 1, @@ -41,22 +41,22 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { content = { Column { SubBlock( - title = stringResource(id = R.string.markets_token_details_official_links), + title = stringResourceSafe(id = R.string.markets_token_details_official_links), links = state.officialLinks, onLinkClick = state.onLinkClick, ) SubBlock( - title = stringResource(id = R.string.markets_token_details_social), + title = stringResourceSafe(id = R.string.markets_token_details_social), links = state.social, onLinkClick = state.onLinkClick, ) SubBlock( - title = stringResource(id = R.string.markets_token_details_repository), + title = stringResourceSafe(id = R.string.markets_token_details_repository), links = state.repository, onLinkClick = state.onLinkClick, ) SubBlock( - title = stringResource(id = R.string.markets_token_details_blockchain_site), + title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site), links = state.blockchainSite, onLinkClick = state.onLinkClick, lastBlock = true, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt index 4825c2ab32..9d33b61034 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt @@ -1,11 +1,12 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.TextButton @@ -14,6 +15,7 @@ import com.tangem.core.ui.components.block.information.GridItems import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer @@ -33,7 +35,7 @@ internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { modifier = modifier, title = { Text( - text = stringResource(id = R.string.markets_token_details_metrics), + text = stringResourceSafe(id = R.string.markets_token_details_metrics), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, maxLines = 1, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index c111de7630..1c63ecd0b4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -2,14 +2,12 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.layout.* -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -21,13 +19,14 @@ import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.progressbar.LinearProgressIndicator import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.features.markets.details.impl.ui.getText +import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf @@ -39,7 +38,7 @@ internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier modifier = modifier, title = { Text( - text = stringResource(id = R.string.markets_token_details_price_performance), + text = stringResourceSafe(id = R.string.markets_token_details_price_performance), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, maxLines = 1, @@ -106,13 +105,13 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = stringResource(R.string.markets_token_details_low), + text = stringResourceSafe(R.string.markets_token_details_low), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) SpacerW8() Text( - text = stringResource(R.string.markets_token_details_high), + text = stringResourceSafe(R.string.markets_token_details_high), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt index 07019d64e1..391e6b8969 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt @@ -181,7 +181,7 @@ private fun SecurityScoreBottomSheetPreview() { TangemThemePreview { SecurityScoreBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = SecurityScorePreviewData.bottomSheetContent, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 79865f0661..ae0a8b3e09 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -1,23 +1,26 @@ package com.tangem.features.markets.entry.impl -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState -import com.arkivanov.decompose.router.stack.* +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.Router -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.message.EventMessageEffect import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.toSerializableParam +import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.domain.markets.toSerializableParam import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent import dagger.assisted.Assisted @@ -28,7 +31,6 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsEntryComponent @AssistedInject constructor( @Assisted context: AppComponentContext, private val marketsEntryChildFactory: MarketsEntryChildFactory, - private val uiDependencies: UiDependencies, ) : MarketsEntryComponent, AppComponentContext by context { private val stackNavigation = StackNavigation() @@ -64,10 +66,6 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( stackState = stack.subscribeAsState(), modifier = modifier, ) - EventMessageEffect( - messageHandler = uiDependencies.eventMessageHandler, - snackbarHostState = uiDependencies.globalSnackbarHostState, - ) } @OptIn(ExperimentalDecomposeApi::class) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index a337b02336..5f82c6c481 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -38,19 +38,21 @@ internal class AddToPortfolioBSContentUMFactory( /** * Create [TangemBottomSheetConfig] * + * @param portfolioData portfolio data * @param portfolioUIData portfolio bottom sheet visibility model * @param selectedWallet selected wallet * @param alreadyAddedNetworks already added networks */ fun create( + currentState: TangemBottomSheetConfig?, portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, selectedWallet: UserWallet?, alreadyAddedNetworks: Set?, ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, + return (currentState ?: TangemBottomSheetConfig.Empty).copy( + isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, content = if (selectedWallet != null && alreadyAddedNetworks != null) { AddToPortfolioBSContentUM( @@ -107,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory( selectedWalletId: UserWalletId, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( - isShow = isShow, + isShown = isShow, onDismissRequest = { onWalletSelectorVisibilityChange(false) }, content = WalletSelectorBSContentUM( userWallets = portfolioData.walletsWithCurrencies diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index c55ba95bec..68b432f633 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -26,7 +26,6 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader -import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider @@ -259,25 +258,22 @@ internal class MarketsPortfolioModel @Inject constructor( } private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = ContentMessage { onDismiss -> - WarningDialog( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - onDismiss = onDismiss, - ) - } + val message = DialogMessage( + message = when (unsupportedState) { + is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + }, + ) messageSender.send(message) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 1bc2ecf543..5729f632b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -14,8 +14,11 @@ import kotlinx.collections.immutable.ImmutableList /** * Factory for creating [MyPortfolioUM] * - * @property onAddClick callback when user wants to add new token - * @property onTokenItemClick callback when user wants to see actions with token + * @property onAddClick callback when user wants to add new token + * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content + * @property tokenActionsHandler token actions handler + * @property currentState current state provider + * @property updateTokens callback for updating tokens * [REDACTED_AUTHOR] */ @@ -87,6 +90,7 @@ internal class MyPortfolioUMFactory( ?.toSet() return addToPortfolioBSContentUMFactory.create( + currentState = currentState().addToPortfolioBSConfig, portfolioData = portfolioData, portfolioUIData = portfolioUIData, selectedWallet = selectedWallet, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index ce9e2b7841..2ca208e28b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase @@ -21,7 +21,6 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.Provider @@ -84,13 +83,9 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun showDemoModeWarning() { - val message = ContentMessage { onDismiss -> - WarningDialog( - message = resourceReference(R.string.alert_demo_feature_disabled), - onDismiss = onDismiss, - ) - } - + val message = DialogMessage( + message = resourceReference(R.string.alert_demo_feature_disabled), + ) messageSender.send(message) } @@ -101,10 +96,10 @@ internal class TokenActionsHandler @AssistedInject constructor( updateTokenReceiveBSConfig { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = { updateTokenReceiveBSConfig { - it.copy(isShow = false) + it.copy(isShown = false) } }, content = TokenReceiveBottomSheetConfig( @@ -130,7 +125,7 @@ internal class TokenActionsHandler @AssistedInject constructor( .toImmutableList() val defaultAddress = addresses.firstOrNull()?.value ?: return - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 73f6dca547..1f0e080b17 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -37,6 +36,7 @@ import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme @@ -154,7 +154,7 @@ private fun ContinueButton( ) .navigationBarsPadding() .fillMaxWidth(), - text = stringResource(R.string.common_continue), + text = stringResourceSafe(R.string.common_continue), icon = if (enabled && isTangemIconVisible) { TangemButtonIconPosition.End(R.drawable.ic_tangem_24) } else { @@ -178,7 +178,7 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi modifier = modifier, title = { Text( - text = stringResource(R.string.markets_select_network), + text = stringResourceSafe(R.string.markets_select_network), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -279,7 +279,7 @@ private fun ScanWalletWarning(modifier: Modifier = Modifier) { contentDescription = null, ) Text( - text = stringResource(R.string.markets_generate_addresses_notification), + text = stringResourceSafe(R.string.markets_generate_addresses_notification), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -295,7 +295,7 @@ private fun Preview( TangemThemePreview { AddToPortfolioBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, content = content, onDismissRequest = {}, ), @@ -370,7 +370,7 @@ private fun PreviewContentTestOnDevice( AddToPortfolioBottomSheet( config = TangemBottomSheetConfig( - isShow = isShow, + isShown = isShow, content = contentState, onDismissRequest = { isShow = false }, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index abd9b2ccdc..115a39f0bc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.util.fastForEachIndexed @@ -24,6 +23,7 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R @@ -58,17 +58,16 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { } } - when (state) { - is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) - is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) - else -> Unit + val bsConfig = state.addToPortfolioBSConfig + if (bsConfig != null) { + AddToPortfolioBottomSheet(config = bsConfig) } } @Composable private fun Title() { Text( - text = stringResource(R.string.markets_common_my_portfolio), + text = stringResourceSafe(R.string.markets_common_my_portfolio), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -150,7 +149,7 @@ fun UnavailableAssetForWallet(modifier: Modifier = Modifier) { private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) { Text( modifier = modifier, - text = stringResource(textId), + text = stringResourceSafe(textId), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) @@ -163,14 +162,14 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Text( - text = stringResource(R.string.markets_add_to_my_portfolio_description), + text = stringResourceSafe(R.string.markets_add_to_my_portfolio_description), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.markets_add_to_portfolio_button), + text = stringResourceSafe(R.string.markets_add_to_portfolio_button), onClick = state.onAddClick, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index 611315a24b..8a09c1758a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -72,7 +72,7 @@ private fun Preview() { Box(Modifier.background(TangemTheme.colors.background.secondary)) { TokenActionsBottomSheet( TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = TokenActionsBSContentUM( title = "Wallet 1", diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt index 22ba89a8f7..a9fdbdaef9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -94,7 +94,7 @@ private fun Preview() { TangemThemePreview { WalletSelectorBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = WalletSelectorBSContentUM( userWallets = persistentListOf( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt deleted file mode 100644 index 70e52d864c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.markets.impl.R - -@Composable -internal fun WarningDialog( - message: TextReference, - onDismiss: () -> Unit, - title: TextReference = resourceReference(R.string.common_warning), -) { - BasicDialog( - title = title.resolveReference(), - message = message.resolveReference(), - confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt index 104b4fc5e6..5d3286bdb5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -7,10 +7,12 @@ import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class MyPortfolioUM { + abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? + data class Tokens( + override val addToPortfolioBSConfig: TangemBottomSheetConfig, val tokens: ImmutableList, val buttonState: AddButtonState, - val addToPortfolioBSConfig: TangemBottomSheetConfig, val tokenReceiveBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() { @@ -23,13 +25,19 @@ internal sealed class MyPortfolioUM { } data class AddFirstToken( - val addToPortfolioBSConfig: TangemBottomSheetConfig, + override val addToPortfolioBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() - data object Loading : MyPortfolioUM() + data object Loading : MyPortfolioUM() { + override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null + } - data object Unavailable : MyPortfolioUM() + data object Unavailable : MyPortfolioUM() { + override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null + } - data object UnavailableForWallet : MyPortfolioUM() + data object UnavailableForWallet : MyPortfolioUM() { + override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 8191f0addf..8f233b53ab 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.charts.MarketChartMini @@ -21,6 +20,7 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.details.impl.model.formatter.toChartType @@ -75,7 +75,7 @@ private fun LeftSide( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), ) { Text( - text = stringResource(id = R.string.wallet_marketplace_block_title, symbol), + text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, symbol), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) @@ -99,7 +99,7 @@ private fun LeftSide( ) Text( modifier = Modifier.alignByBaseline(), - text = stringResource(id = R.string.wallet_marketprice_block_update_time), + text = stringResourceSafe(id = R.string.wallet_marketprice_block_update_time), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 66801cb9bd..6c2b177d24 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -26,8 +26,8 @@ internal class MarketsListUMStateManager( ) { private var sortByBottomSheetIsShown - get() = state.value.sortByBottomSheet.isShow - set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShow = value)) } + get() = state.value.sortByBottomSheet.isShown + set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShown = value)) } var searchQuery get() = state.value.searchBar.query @@ -203,7 +203,7 @@ internal class MarketsListUMStateManager( onIntervalClick = { selectedInterval = it }, onSortByButtonClick = { sortByBottomSheetIsShown = true }, sortByBottomSheet = TangemBottomSheetConfig( - isShow = false, + isShown = false, onDismissRequest = { sortByBottomSheetIsShown = false }, content = SortByBottomSheetContentUM( selectedOption = SortByTypeUM.Rating, @@ -217,7 +217,7 @@ internal class MarketsListUMStateManager( it.copy( selectedSortBy = sortByTypeUM, sortByBottomSheet = it.sortByBottomSheet.copy( - isShow = false, + isShown = false, content = (it.sortByBottomSheet.content as SortByBottomSheetContentUM).copy( selectedOption = sortByTypeUM, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 71d013cc58..752717b1bb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -33,6 +32,7 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -62,7 +62,7 @@ internal fun MarketsList( ) MarketsListSortByBottomSheet(config = state.sortByBottomSheet) KeyboardEvents( - isSortByBottomSheetShown = state.sortByBottomSheet.isShow, + isSortByBottomSheetShown = state.sortByBottomSheet.isShown, bottomSheetState = bottomSheetState, ) } @@ -149,9 +149,9 @@ private fun Title(isInSearchMode: Boolean, modifier: Modifier = Modifier) { Text( modifier = modifier, text = if (isInSearchMode) { - stringResource(id = R.string.markets_search_result_title) + stringResourceSafe(id = R.string.markets_search_result_title) } else { - stringResource(id = R.string.markets_common_title) + stringResourceSafe(id = R.string.markets_common_title) }, style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, @@ -321,7 +321,7 @@ private fun Preview() { onIntervalClick = {}, onSortByButtonClick = {}, sortByBottomSheet = TangemBottomSheetConfig( - isShow = false, + isShown = false, onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index f4dd833503..77b086ff3b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -10,12 +10,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM @@ -160,7 +160,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.markets_search_see_tokens_under_100k), + text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) @@ -180,7 +180,7 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) { contentAlignment = Alignment.Center, ) { Text( - text = stringResource(R.string.markets_search_token_no_result_title), + text = stringResourceSafe(R.string.markets_search_token_no_result_title), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt index 61b289ff7e..9f08d614c4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt @@ -72,7 +72,7 @@ private fun Preview() { Box(Modifier.background(TangemTheme.colors.background.secondary)) { MarketsListSortByBottomSheet( TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = SortByBottomSheetContentUM( selectedOption = SortByTypeUM.Trending, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt index 4cec85a065..c96d848bac 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt @@ -7,11 +7,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R @@ -24,7 +24,7 @@ internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Mod horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.markets_loading_error_title), + text = stringResourceSafe(R.string.markets_loading_error_title), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt index e86227d668..17841eaf7b 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.onboarding.v2 interface OnboardingV2FeatureToggles { val isOnboardingV2Enabled: Boolean + val isVisaOnboardingEnabled: Boolean } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt index 529bebe5d7..956f744182 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -8,4 +8,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor( ) : OnboardingV2FeatureToggles { override val isOnboardingV2Enabled: Boolean get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED") + override val isVisaOnboardingEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED") } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt index d357369c9f..f3c8aa0111 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.airbnb.lottie.compose.LottieAnimation @@ -17,6 +17,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -49,16 +50,17 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo SpacerH32() Text( modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.onboarding_done_header), + text = stringResourceSafe(R.string.onboarding_done_header), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH12() Text( modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.onboarding_subtitle_success_tangem_wallet_onboarding), + text = stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, ) SpacerH(72.dp) } @@ -69,7 +71,7 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo .padding(horizontal = 16.dp) .padding(bottom = 16.dp) .navigationBarsPadding(), - text = stringResource(R.string.onboarding_button_continue_wallet), + text = stringResourceSafe(R.string.onboarding_button_continue_wallet), onClick = onContinueClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 5bf0212a80..ef564c6194 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -69,6 +69,10 @@ internal class OnboardingEntryModel @Inject constructor( onDone = ::onMultiWalletOnboardingDone, mode = multiWalletNavigationMode, ) + ProductType.Visa -> OnboardingRoute.Visa( + scanResponse = scanResponse, + titleProvider = titleProvider, + ) else -> error("Unsupported") } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 3721bc7927..a7488de99a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -4,12 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import javax.inject.Inject internal class OnboardingChildFactory @Inject constructor( private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory, private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, + private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, ) { fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any { @@ -24,6 +26,13 @@ internal class OnboardingChildFactory @Inject constructor( mode = route.mode, ), ) + is OnboardingRoute.Visa -> onBoardingVisaComponentFactory.create( + context = childContext, + params = OnboardingVisaComponent.Params( + scanResponse = route.scanResponse, + titleProvider = route.titleProvider, + ), + ) is OnboardingRoute.ManageTokens -> onboardingManageTokensComponentFactory.create( context = childContext, params = OnboardingManageTokensComponent.Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 01618cbde2..090724dff0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -18,6 +18,11 @@ sealed class OnboardingRoute : Route { val onDone: (UserWallet) -> Unit, ) : OnboardingRoute() + data class Visa( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + ) : OnboardingRoute() + data class ManageTokens( val userWallet: UserWallet, ) : OnboardingRoute() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt index 1c174b06a6..e9d2be883a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt @@ -47,6 +47,11 @@ internal inline fun OnboardingEntry( modifier = modifier, ) } + is OnboardingRoute.Visa -> { + (it.instance as ComposableContentComponent).Content( + modifier = modifier, + ) + } OnboardingRoute.None -> {} } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index 750a119975..6e7321f878 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -33,6 +33,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWall import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.Wallet1ChooseOptionComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.Wallet1ScanPrimaryComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState @@ -59,6 +60,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor CreateWallet -> WalletArtworksState.Folded ChooseBackupOption -> WalletArtworksState.Fan SeedPhrase -> WalletArtworksState.Folded + ScanPrimary -> WalletArtworksState.Folded AddBackupDevice -> WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First) Finalize -> WalletArtworksState.Stack() Done -> error("Done state should not be used as starting state") @@ -93,11 +95,14 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor ) val backButtonClickFlow = MutableSharedFlow() + override val innerNavigation: InnerNavigation = object : InnerNavigation { override val state = innerNavigationStateFlow override fun pop(onComplete: (Boolean) -> Unit) { - if (childStack.active.configuration == SeedPhrase) { + val config = childStack.active.configuration + + if (config == SeedPhrase || config == Finalize) { componentScope.launch { backButtonClickFlow.emit(Unit) } } else { model.onBack() @@ -152,6 +157,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor backButtonClickFlow = backButtonClickFlow, onBack = { stackNavigation.pop() }, ) + ScanPrimary -> Wallet1ScanPrimaryComponent( + context = childContext, + params = childParams, + onDone = { handleNavigationEvent(AddBackupDevice) }, + ) AddBackupDevice -> MultiWalletBackupComponent( context = childContext, params = childParams, @@ -160,6 +170,8 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor Finalize -> MultiWalletFinalizeComponent( context = childContext, params = childParams, + backButtonClickFlow = backButtonClickFlow, + onBack = { model.onBack() }, onEvent = ::handleFinalizeComponentEvent, ) Done -> error("Unexpected Done state") diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/MultiWalletAccessCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/MultiWalletAccessCodeComponent.kt index 9edd571b93..7beeb89bd9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/MultiWalletAccessCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/MultiWalletAccessCodeComponent.kt @@ -51,7 +51,7 @@ class MultiWalletAccessCodeComponent( val bsConfig = remember(this, showBs) { TangemBottomSheetConfig( - isShow = showBs, + isShown = showBs, onDismissRequest = { dismiss() }, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt index 2f2a18e497..e4b8f341f3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model +import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped @@ -20,6 +21,7 @@ import javax.inject.Inject private const val MINIMUM_ACCESS_CODE_LENGTH = 4 +@Stable @ComponentScoped internal class MultiWalletAccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt index ca6f0b7c5f..0eee40db84 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt @@ -4,12 +4,12 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM @@ -65,9 +65,9 @@ private fun Content(state: MultiWalletAccessCodeUM, modifier: Modifier = Modifie .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .imePadding(), text = if (state.step == ConfirmAccessCode) { - stringResource(R.string.common_confirm) + stringResourceSafe(R.string.common_confirm) } else { - stringResource(R.string.common_continue) + stringResourceSafe(R.string.common_continue) }, onClick = state.onContinue, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index 4aa51c8e79..38e68c7642 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -1,6 +1,9 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -9,12 +12,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.OutlineTextFieldWithIcon +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -35,17 +38,17 @@ internal fun MultiWalletAccessCodeEnter( ) { Text( text = if (reEnterAccessCodeState) { - stringResource(R.string.onboarding_access_code_repeat_code_title) + stringResourceSafe(R.string.onboarding_access_code_repeat_code_title) } else { - stringResource(R.string.onboarding_access_code_intro_title) + stringResourceSafe(R.string.onboarding_access_code_intro_title) }, style = TangemTheme.typography.subtitle1, ) Text( text = if (reEnterAccessCodeState) { - stringResource(R.string.onboarding_access_code_repeat_code_hint) + stringResourceSafe(R.string.onboarding_access_code_repeat_code_hint) } else { - stringResource(R.string.onboarding_access_code_hint) + stringResourceSafe(R.string.onboarding_access_code_hint) }, style = TangemTheme.typography.body1, ) @@ -73,7 +76,7 @@ internal fun MultiWalletAccessCodeEnter( } else { state.onAccessCodeFirstChange }, - label = stringResource(id = R.string.onboarding_wallet_info_title_third), + label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), isError = state.codesNotMatchError || state.atLeast4CharError, visualTransformation = if (state.accessCodeHidden) { PasswordVisualTransformation() @@ -82,9 +85,9 @@ internal fun MultiWalletAccessCodeEnter( }, caption = when { state.codesNotMatchError && reEnterAccessCodeState -> - stringResource(R.string.onboarding_access_codes_doesnt_match) + stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) state.atLeast4CharError && !reEnterAccessCodeState -> - stringResource(R.string.onboarding_access_code_too_short) + stringResourceSafe(R.string.onboarding_access_code_too_short) else -> null }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt index 3172592edb..0b4c0fe713 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt @@ -8,12 +8,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -26,7 +26,7 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = stringResource(R.string.onboarding_access_code_intro_title), + text = stringResourceSafe(R.string.onboarding_access_code_intro_title), style = TangemTheme.typography.h2, ) Icon( @@ -41,18 +41,18 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(24.dp), ) { DescriptionItem( - title = stringResource(R.string.onboarding_access_code_feature_1_title), - body = stringResource(R.string.onboarding_access_code_feature_1_description), + title = stringResourceSafe(R.string.onboarding_access_code_feature_1_title), + body = stringResourceSafe(R.string.onboarding_access_code_feature_1_description), iconRes = R.drawable.ic_feature_1, ) DescriptionItem( - title = stringResource(R.string.onboarding_access_code_feature_2_title), - body = stringResource(R.string.onboarding_access_code_feature_2_description), + title = stringResourceSafe(R.string.onboarding_access_code_feature_2_title), + body = stringResourceSafe(R.string.onboarding_access_code_feature_2_description), iconRes = R.drawable.ic_feature_2, ) DescriptionItem( - title = stringResource(R.string.onboarding_access_code_feature_3_title), - body = stringResource(R.string.onboarding_access_code_feature_3_description), + title = stringResourceSafe(R.string.onboarding_access_code_feature_3_title), + body = stringResourceSafe(R.string.onboarding_access_code_feature_3_description), iconRes = R.drawable.ic_feature_3, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt index a38f4c6a9a..6b63fc8c48 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt @@ -8,8 +8,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.MultiWalletBackup import kotlinx.coroutines.flow.update diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index ecf5661097..9d8fb21173 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model +import androidx.compose.runtime.Stable import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError @@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @ComponentScoped class MultiWalletBackupModel @Inject constructor( paramsContainer: ParamsContainer, @@ -82,15 +84,16 @@ class MultiWalletBackupModel @Inject constructor( } private fun startBackupWallet() { - if (state.value.numberOfBackupCards == 0) { + if (state.value.numberOfBackupCards == 0 && scanResponse.primaryCard != null) { backupService.discardSavedBackup() } - val primaryCard = scanResponse.primaryCard + val primaryCard = scanResponse.primaryCard if (primaryCard != null) { backupService.setPrimaryCard(primaryCard) - addBackupCardWithService() } + + addBackupCardWithService() } private fun setNumberOfBackupCards(number: Int) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt index 9bf7b55ef5..f82b028c2a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt @@ -1,17 +1,22 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButtonIconEnd import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -55,7 +60,8 @@ private fun ColumnScope.Content(state: MultiWalletBackupUM) { Column( Modifier .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 74.dp) + .verticalScroll(rememberScrollState()) + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -83,7 +89,7 @@ private fun ColumnScope.Content(state: MultiWalletBackupUM) { iconResId = R.drawable.ic_tangem_24, enabled = state.addBackupButtonEnabled, showProgress = state.addBackupButtonLoading, - text = stringResource(R.string.onboarding_button_add_backup_card), + text = stringResourceSafe(R.string.onboarding_button_add_backup_card), onClick = state.onAddBackupClick, ) @@ -91,7 +97,7 @@ private fun ColumnScope.Content(state: MultiWalletBackupUM) { modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = stringResource(R.string.onboarding_button_finalize_backup), + text = stringResourceSafe(R.string.onboarding_button_finalize_backup), enabled = state.finalizeButtonEnabled, onClick = state.onFinalizeButtonClick, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt index c5f8c63d19..33bd477297 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt @@ -32,7 +32,7 @@ class Wallet1ChooseOptionComponent( } params.parentParams.titleProvider.changeTitle( - text = resourceReference(R.string.onboarding_create_wallet_header), + text = resourceReference(R.string.onboarding_getting_started), ) componentScope.launch { @@ -45,6 +45,7 @@ class Wallet1ChooseOptionComponent( @Composable override fun Content(modifier: Modifier) { Wallet1ChooseOption( + canSkipBackup = model.canSkipBackup, onBackupClick = { onNextStep(OnboardingMultiWalletState.Step.AddBackupDevice) }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index 0577ed1513..69d37f496f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -1,10 +1,12 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.model +import androidx.compose.runtime.Stable import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet @@ -18,6 +20,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @ComponentScoped internal class Wallet1ChooseOptionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -36,6 +39,8 @@ internal class Wallet1ChooseOptionModel @Inject constructor( analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened) } + val canSkipBackup = params.multiWalletState.value.currentScanResponse.card.canSkipBackup + fun onSkipClick() { if (skipClicked) return skipClicked = true diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt index 049dd64f2b..b873a9e6b8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt @@ -1,20 +1,29 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @Composable -fun Wallet1ChooseOption(onSkipClick: () -> Unit, onBackupClick: () -> Unit, modifier: Modifier = Modifier) { +fun Wallet1ChooseOption( + canSkipBackup: Boolean, + onSkipClick: () -> Unit, + onBackupClick: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxSize() @@ -26,12 +35,13 @@ fun Wallet1ChooseOption(onSkipClick: () -> Unit, onBackupClick: () -> Unit, modi Column( Modifier .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 24.dp) + .verticalScroll(rememberScrollState()) + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.onboarding_create_wallet_header), + text = stringResourceSafe(R.string.onboarding_wallet_info_title_first), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -39,7 +49,7 @@ fun Wallet1ChooseOption(onSkipClick: () -> Unit, onBackupClick: () -> Unit, modi ) Text( - text = stringResource(R.string.onboarding_create_wallet_body), + text = stringResourceSafe(R.string.onboarding_wallet_info_subtitle_first), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -47,21 +57,35 @@ fun Wallet1ChooseOption(onSkipClick: () -> Unit, onBackupClick: () -> Unit, modi ) } - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, - text = stringResource(R.string.onboarding_button_backup_now), + text = stringResourceSafe(R.string.onboarding_button_backup_now), onClick = onBackupClick, ) - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResource(R.string.onboarding_button_skip_backup), - onClick = onSkipClick, + if (canSkipBackup) { + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_button_skip_backup), + onClick = onSkipClick, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + Wallet1ChooseOption( + canSkipBackup = true, + onSkipClick = {}, + onBackupClick = {}, + modifier = Modifier.fillMaxSize(), ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt index f23f57f93a..88f37db96d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.lifecycle.doOnStart import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.resourceReference @@ -25,17 +26,19 @@ internal class MultiWalletCreateWalletComponent( private val model: MultiWalletCreateWalletModel = getOrCreateModel(params) init { - params.innerNavigation.update { - it.copy( - stackSize = 2, - stackMaxSize = 8, + lifecycle.doOnStart { + params.innerNavigation.update { + it.copy( + stackSize = 2, + stackMaxSize = 8, + ) + } + + params.parentParams.titleProvider.changeTitle( + text = resourceReference(R.string.onboarding_create_wallet_header), ) } - params.parentParams.titleProvider.changeTitle( - text = resourceReference(R.string.onboarding_create_wallet_header), - ) - componentScope.launch { model.onDone.collect(onNextStep) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index b7b153a6f7..c41f5bdff9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model +import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -11,12 +12,17 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog -import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -27,6 +33,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("LongParameterList") +@Stable @ComponentScoped internal class MultiWalletCreateWalletModel @Inject constructor( paramsContainer: ParamsContainer, @@ -36,6 +43,8 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val cardRepository: CardRepository, private val analyticsHandler: AnalyticsEventHandler, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { private val params = paramsContainer.require() @@ -61,7 +70,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( showOtherOptionsButton = params.parentParams.withSeedPhraseFlow, onOtherOptionsClick = { modelScope.launch { - onDone.emit(OnboardingMultiWalletState.Step.SeedPhrase) + onDone.emit(Step.SeedPhrase) } }, dialog = null, @@ -69,7 +78,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( ) val uiState: StateFlow = _uiState - val onDone = MutableSharedFlow() + val onDone = MutableSharedFlow() init { analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened) @@ -96,13 +105,14 @@ internal class MultiWalletCreateWalletModel @Inject constructor( cardRepository.startCardActivation(cardId = result.data.card.cardId) - if (params.parentParams.withSeedPhraseFlow) { - onDone.emit(OnboardingMultiWalletState.Step.AddBackupDevice) - } else { - onDone.emit(OnboardingMultiWalletState.Step.ChooseBackupOption) - } - analyticsHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + + val cardDoesNotSupportBackup = result.data.card.settings.isBackupAllowed.not() + when { + cardDoesNotSupportBackup -> createWalletAndNavigateBackWithDone() + params.parentParams.withSeedPhraseFlow -> onDone.emit(Step.AddBackupDevice) + else -> onDone.emit(Step.ChooseBackupOption) + } } is CompletionResult.Failure -> { @@ -115,6 +125,32 @@ internal class MultiWalletCreateWalletModel @Inject constructor( } } + private fun createWalletAndNavigateBackWithDone() { + modelScope.launch { + val scanResponse = params.multiWalletState.value.currentScanResponse + + val userWallet = createUserWallet(scanResponse) + saveWalletUseCase(userWallet, canOverride = true) + .onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) + + // save user wallet for manage tokens screen + params.multiWalletState.update { + it.copy(resultUserWallet = userWallet) + } + + onDone.emit(Step.Done) + } + } + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { + return requireNotNull( + value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(), + lazyMessage = { "User wallet not created" }, + ) + } + private fun handleActivationError() { _uiState.update { state -> state.copy( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt index a8ff5a761f..0404814a1f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt @@ -1,11 +1,12 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -48,7 +50,8 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Column( Modifier .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 24.dp) + .verticalScroll(rememberScrollState()) + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -74,7 +77,7 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) .fillMaxWidth(), iconResId = R.drawable.ic_tangem_24, - text = stringResource(R.string.onboarding_create_wallet_button_create_wallet), + text = stringResourceSafe(R.string.onboarding_create_wallet_button_create_wallet), onClick = state.onCreateWalletClick, ) @@ -83,7 +86,7 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = stringResource(R.string.onboarding_create_wallet_options_button_options), + text = stringResourceSafe(R.string.onboarding_create_wallet_options_button_options), onClick = state.onOtherOptionsClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt index c6ebe6cd81..22025b3798 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -12,6 +13,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.MultiWalletFinalize +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -19,6 +21,8 @@ import kotlinx.coroutines.launch internal class MultiWalletFinalizeComponent( context: AppComponentContext, params: MultiWalletChildParams, + backButtonClickFlow: SharedFlow, + onBack: () -> Unit, onEvent: (Event) -> Unit, ) : AppComponentContext by context, MultiWalletChildComponent { @@ -40,12 +44,21 @@ internal class MultiWalletFinalizeComponent( onEvent(it) } } + + componentScope.launch { + backButtonClickFlow.collect { model.onBack() } + } + componentScope.launch { + model.onBackFlow.collect { onBack() } + } } @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + BackHandler { model.onBack() } + MultiWalletFinalize( state = state, modifier = modifier, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 805acccfca..6116ad29e2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model +import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.decompose.di.ComponentScoped @@ -29,14 +30,12 @@ import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.StringsSigns import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("LongParameterList") +@Stable @ComponentScoped internal class MultiWalletFinalizeModel @Inject constructor( paramsContainer: ParamsContainer, @@ -59,6 +58,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private var walletHasBackupError = false val uiState = _uiState.asStateFlow() + val onBackFlow = MutableSharedFlow() val onEvent = MutableSharedFlow() @@ -72,10 +72,31 @@ internal class MultiWalletFinalizeModel @Inject constructor( } } + fun onBack() { + if (uiState.value.scanPrimary) { + modelScope.launch { onBackFlow.emit(Unit) } + } + } + private fun getInitialState(): MultiWalletFinalizeUM { val backupService = backupServiceHolder.backupService.get() ?: return MultiWalletFinalizeUM() val initialStep = getInitialStep() + // sets proper artwork state for initial step + // (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY]) + modelScope.launch { + when (initialStep) { + MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ } + MultiWalletFinalizeUM.Step.BackupDevice1 -> { + onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) + } + MultiWalletFinalizeUM.Step.BackupDevice2 -> { + onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) + onEvent.emit(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded) + } + } + } + val batchId = when (initialStep) { MultiWalletFinalizeUM.Step.Primary -> backupService.primaryCardBatchId MultiWalletFinalizeUM.Step.BackupDevice1 -> backupService.backupCardsBatchIds.getOrNull(0) @@ -193,18 +214,24 @@ internal class MultiWalletFinalizeModel @Inject constructor( private fun finishBackup() { modelScope.launch { val scanResponse = params.multiWalletState.value.currentScanResponse - val userWallet = createUserWallet(scanResponse) - when (params.parentParams.mode) { + val userWalletCreated = createUserWallet(scanResponse) + + val userWallet = when (params.parentParams.mode) { OnboardingMultiWalletComponent.Mode.Onboarding -> { userWalletsListManager.save( - userWallet = userWallet.copy( + userWallet = userWalletCreated.copy( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), canOverride = true, ) + userWalletCreated } OnboardingMultiWalletComponent.Mode.AddBackup -> { + val userWallet = userWalletsListManager.userWallets.first() + .firstOrNull { it.scanResponse.primaryCard?.cardId == scanResponse.primaryCard?.cardId } + ?: userWalletCreated + userWalletsListManager.update( userWalletId = userWallet.walletId, update = { wallet -> @@ -213,6 +240,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( ) }, ) + + userWallet } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt index 9bbfe88b5e..24a3780233 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt @@ -1,15 +1,17 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -27,13 +29,14 @@ internal fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifie Column( Modifier .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 74.dp) + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .verticalScroll(rememberScrollState()) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.padding(top = 16.dp), - text = stringResource( + text = stringResourceSafe( when { state.scanPrimary && state.isRing -> R.string.common_origin_ring state.scanPrimary && state.isRing.not() -> R.string.common_origin_card @@ -49,9 +52,9 @@ internal fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifie Text( modifier = Modifier.padding(top = 12.dp), text = if (state.isRing) { - stringResource(R.string.onboarding_subtitle_scan_ring) + stringResourceSafe(R.string.onboarding_subtitle_scan_ring) } else { - stringResource( + stringResourceSafe( R.string.onboarding_subtitle_scan_backup_card_format, state.cardNumber, ) @@ -66,7 +69,7 @@ internal fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifie modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = stringResource( + text = stringResourceSafe( if (state.isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin, ), iconResId = R.drawable.ic_tangem_24, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/Wallet1ScanPrimaryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/Wallet1ScanPrimaryComponent.kt new file mode 100644 index 0000000000..a6d80fd63b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/Wallet1ScanPrimaryComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.Wallet1ScanPrimaryModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.ui.Wallet1ScanPrimary +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class Wallet1ScanPrimaryComponent( + context: AppComponentContext, + params: MultiWalletChildParams, + onDone: () -> Unit, +) : AppComponentContext by context, ComposableContentComponent { + + private val model: Wallet1ScanPrimaryModel = getOrCreateModel(params) + + init { + params.innerNavigation.update { + it.copy( + stackSize = 4, + stackMaxSize = 8, + ) + } + + params.parentParams.titleProvider.changeTitle( + text = resourceReference(R.string.onboarding_navbar_title_creating_backup), + ) + + componentScope.launch { model.onDone.collect { onDone() } } + } + + @Composable + override fun Content(modifier: Modifier) { + Wallet1ScanPrimary( + isRing = model.isRing, + onScanPrimaryClick = model::onScanPrimaryClick, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/model/Wallet1ScanPrimaryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/model/Wallet1ScanPrimaryModel.kt new file mode 100644 index 0000000000..504e556fbc --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/model/Wallet1ScanPrimaryModel.kt @@ -0,0 +1,43 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model + +import androidx.compose.runtime.Stable +import com.tangem.common.CompletionResult +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.sdk.api.BackupServiceHolder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class Wallet1ScanPrimaryModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val backupServiceHolder: BackupServiceHolder, +) : Model() { + + private val params = paramsContainer.require() + private val scanResponse = params.multiWalletState.value.currentScanResponse + + val isRing = scanResponse.cardTypesResolver.isRing() + val onDone = MutableSharedFlow() + + fun onScanPrimaryClick() { + val backupService = backupServiceHolder.backupService.get() ?: return + val iconScanRes = R.drawable.img_hand_scan_ring.takeIf { isRing } + backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = scanResponse.card.cardId) { result -> + when (result) { + is CompletionResult.Success -> { + modelScope.launch { onDone.emit(Unit) } + } + is CompletionResult.Failure -> Unit + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/ui/Wallet1ScanPrimary.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/ui/Wallet1ScanPrimary.kt new file mode 100644 index 0000000000..e15c5a640a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/scanprimary/ui/Wallet1ScanPrimary.kt @@ -0,0 +1,74 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R + +@Composable +internal fun Wallet1ScanPrimary(isRing: Boolean, onScanPrimaryClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + verticalArrangement = Arrangement.Bottom, + ) { + Column( + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .verticalScroll(rememberScrollState()) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(top = 16.dp), + text = stringResourceSafe(R.string.onboarding_title_scan_origin_card), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResourceSafe(R.string.onboarding_subtitle_scan_primary), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe( + if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin, + ), + iconResId = R.drawable.ic_tangem_24, + onClick = onScanPrimaryClick, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + Wallet1ScanPrimary( + isRing = true, + onScanPrimaryClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index ad00ff8019..cf3cf6d141 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model +import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -33,6 +34,7 @@ import javax.inject.Inject // | DON'T FIXME !!! MODIFY WITH CAUTION !!! | // ============================================= @Suppress("LongParameterList") +@Stable @ComponentScoped internal class MultiWalletSeedPhraseModel @Inject constructor( paramsContainer: ParamsContainer, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt index af12fa1b39..dbfeee4003 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.tangem.utils.SupportedLanguages import java.util.Locale +// TODO: [REDACTED_JIRA] internal fun seedPhraseLearnMoreUrl(): String { val language = Locale.getDefault().language diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/ImportSeedPhraseUiStateBuilder.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/ImportSeedPhraseUiStateBuilder.kt index d4f152b149..bc73e0a862 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/ImportSeedPhraseUiStateBuilder.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/ImportSeedPhraseUiStateBuilder.kt @@ -157,7 +157,7 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { state -> state.copy( infoBottomSheetConfig = TangemBottomSheetConfig.Empty.copy( - isShow = true, + isShown = true, onDismissRequest = { updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Empty) } }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt index 10c3a39e4c..f64fd62c0d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.OutlinedTextField import androidx.compose.material3.Text @@ -14,8 +15,8 @@ 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.res.stringResource import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -55,49 +57,56 @@ internal fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()), + .imePadding(), ) { - Text( - text = stringResource(id = R.string.onboarding_seed_import_message), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 16.dp) - .fillMaxWidth(), - ) - - PhraseBlock( - state = state, - modifier = Modifier.padding(horizontal = 16.dp), - ) - - OutlineTextFieldWithIcon( - modifier = modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - value = state.passPhrase, - onValueChange = state.passPhraseChange, - iconResId = R.drawable.ic_information_24, - iconColor = TangemTheme.colors.icon.informative, - label = stringResource(id = R.string.common_passphrase), - placeholder = stringResource(id = R.string.send_optional_field), - onIconClick = state.onPassphraseInfoClick, - ) - - Box(Modifier.weight(1f)) { - PrimaryButtonIconEnd( + Column( + Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + ) { + Text( + text = stringResourceSafe(id = R.string.onboarding_seed_import_message), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, modifier = Modifier - .align(Alignment.BottomStart) - .padding(16.dp) + .padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 16.dp) .fillMaxWidth(), - text = stringResource(id = R.string.common_import), - iconResId = R.drawable.ic_tangem_24, - enabled = state.createWalletEnabled, - showProgress = state.createWalletProgress, - onClick = state.createWalletClick, + ) + + PhraseBlock( + state = state, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + OutlineTextFieldWithIcon( + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + value = state.passPhrase, + onValueChange = state.passPhraseChange, + iconResId = R.drawable.ic_information_24, + iconColor = TangemTheme.colors.icon.informative, + label = stringResourceSafe(id = R.string.common_passphrase), + placeholder = stringResourceSafe(id = R.string.send_optional_field), + onIconClick = state.onPassphraseInfoClick, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + ), ) } + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + text = stringResourceSafe(id = R.string.common_import), + iconResId = R.drawable.ic_tangem_24, + enabled = state.createWalletEnabled, + showProgress = state.createWalletProgress, + onClick = state.createWalletClick, + ) } val keyboard by keyboardAsState() @@ -139,6 +148,10 @@ private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifie singleLine = false, colors = TangemTextFieldsDefault.defaultTextFieldColors, visualTransformation = invalidWordsColorTransformation, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + ), ) Box( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt index aff9adb6f1..b889ab83ba 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextAlign @@ -19,6 +18,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -51,7 +51,7 @@ internal fun MultiWalletSeedPhraseStart(state: MultiWalletSeedPhraseUM.Start, mo Text( modifier = Modifier .padding(horizontal = TangemTheme.dimens.size12, vertical = TangemTheme.dimens.size4), - text = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy), + text = stringResourceSafe(id = R.string.onboarding_seed_phrase_intro_legacy), color = TangemTheme.colors.text.warning, style = TangemTheme.typography.body1, ) @@ -71,12 +71,12 @@ private fun BodyContent(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - text = stringResource(id = R.string.onboarding_seed_intro_title), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) Text( - text = stringResource(id = R.string.onboarding_seed_intro_message), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_message), style = TangemTheme.typography.body1 .copy(lineBreak = LineBreak.Paragraph), color = TangemTheme.colors.text.secondary, @@ -119,7 +119,7 @@ private fun ReadMoreBlock(state: MultiWalletSeedPhraseUM.Start, modifier: Modifi Text( modifier = Modifier.align(Alignment.CenterVertically), style = TangemTheme.typography.button, - text = stringResource(id = R.string.onboarding_seed_button_read_more), + text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more), ) } } @@ -135,13 +135,13 @@ private fun Buttons(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = M ) { SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.onboarding_seed_intro_button_generate), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_generate), onClick = state.onGenerateSeedPhraseClicked, ) SecondaryButton( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_seed_intro_button_import), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_import), onClick = state.onImportSeedPhraseClicked, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt index 94ffccee63..1c9b8abf0f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt @@ -8,14 +8,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.pluralStringResourceSafe +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -31,26 +31,32 @@ internal fun MultiWalletSeedPhraseWords( modifier: Modifier = Modifier, ) { Column(modifier.fillMaxSize()) { - SegmentSeedBlock( - modifier = Modifier.padding(vertical = 20.dp), - state = state, - ) - - TitleBlock(state) - - SeedPhraseGridBlock( - mnemonicGridItems = state.words, + Column( modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(top = 20.dp, bottom = 32.dp), - ) + .verticalScroll(rememberScrollState()) + .imePadding() + .weight(1f), + ) { + SegmentSeedBlock( + modifier = Modifier.padding(vertical = 20.dp), + state = state, + ) + + TitleBlock(state) + + SeedPhraseGridBlock( + mnemonicGridItems = state.words, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 32.dp), + ) + } PrimaryButton( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - text = stringResource(id = R.string.common_continue), + text = stringResourceSafe(id = R.string.common_continue), onClick = state.onContinueClick, ) } @@ -70,7 +76,7 @@ private fun SegmentSeedBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, }, ) { Text( - text = pluralStringResource( + text = pluralStringResourceSafe( id = R.plurals.onboarding_seed_generate_words_count, count = if (it == 0) 12 else 24, if (it == 0) 12 else 24, @@ -95,14 +101,14 @@ private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifi verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - text = stringResource(id = R.string.onboarding_seed_generate_title), + text = stringResourceSafe(id = R.string.onboarding_seed_generate_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) Text( - text = pluralStringResource( + text = pluralStringResourceSafe( id = R.plurals.onboarding_seed_generate_message_words_count, count = if (state.words24OptionSelected.not()) 12 else 24, if (state.words24OptionSelected.not()) 12 else 24, @@ -118,8 +124,7 @@ private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifi @Composable private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList, modifier: Modifier = Modifier) { VerticalGrid( - modifier = modifier - .verticalScroll(rememberScrollState()), + modifier = modifier, items = mnemonicGridItems, ) { item -> Row( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt index f8ea2bbdba..dfc2b1cd70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt @@ -2,22 +2,29 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.OutlineTextField import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM @@ -48,64 +55,79 @@ internal fun MultiWalletSeedPhraseWordsCheck( Column( modifier = modifier .fillMaxSize() - .verticalScroll(rememberScrollState()), + .imePadding(), ) { - Text( - text = stringResource(R.string.onboarding_seed_user_validation_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp) - .fillMaxWidth(), - ) - Text( - text = stringResource(R.string.onboarding_seed_user_validation_message), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(horizontal = 36.dp) - .fillMaxWidth(), - ) - - Fields( - state = state, - modifier = Modifier - .padding(vertical = 30.dp, horizontal = 16.dp), - ) - - Box( - Modifier.weight(1f), + Column( + Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), ) { - PrimaryButtonIconEnd( + Text( + text = stringResourceSafe(R.string.onboarding_seed_user_validation_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, modifier = Modifier - .align(Alignment.BottomStart) - .padding(16.dp) + .padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp) .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), - iconResId = R.drawable.ic_tangem_24, - enabled = state.createWalletButtonEnabled, - showProgress = state.createWalletButtonProgress, - onClick = state.onCreateWalletButtonClick, + ) + Text( + text = stringResourceSafe(R.string.onboarding_seed_user_validation_message), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 36.dp) + .fillMaxWidth(), + ) + + Fields( + state = state, + modifier = Modifier + .padding(vertical = 30.dp, horizontal = 16.dp), ) } + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(16.dp) + .imePadding() + .fillMaxWidth(), + text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet), + iconResId = R.drawable.ic_tangem_24, + enabled = state.createWalletButtonEnabled, + showProgress = state.createWalletButtonProgress, + onClick = state.onCreateWalletButtonClick, + ) } } @Composable private fun Fields(state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, modifier: Modifier = Modifier) { + val focusManager = LocalFocusManager.current + Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - state.wordFields.forEach { field -> - OutlineTextField( - value = field.word, - onValueChange = field.onChange, - label = field.index.toString(), - isError = field.error, - ) + DisableContextMenu { + state.wordFields.fastForEachIndexed { index, field -> + OutlineTextField( + value = field.word, + onValueChange = field.onChange, + label = field.index.toString(), + isError = field.error, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next, + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() }, + onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) }, + ), + ) + } } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt index fdf30402ea..99c65af89b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt @@ -11,13 +11,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -50,7 +50,7 @@ fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { ) Text( - text = stringResource(id = R.string.common_passphrase), + text = stringResourceSafe(id = R.string.common_passphrase), modifier = Modifier .padding(top = TangemTheme.dimens.size40) .align(Alignment.CenterHorizontally), @@ -59,7 +59,7 @@ fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { ) Text( - text = stringResource(id = R.string.onboarding_bottom_sheet_passphrase_description), + text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), modifier = Modifier .padding(top = TangemTheme.dimens.size16) .padding(horizontal = TangemTheme.dimens.size24) @@ -75,7 +75,7 @@ fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { .padding(top = TangemTheme.dimens.size40) .padding(bottom = TangemTheme.dimens.size32) .fillMaxWidth(), - text = stringResource(id = R.string.common_ok), + text = stringResourceSafe(id = R.string.common_ok), onClick = onDismiss, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt index 1de409e3a2..1aee88709b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt @@ -9,6 +9,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.Mul import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.model.Wallet1ChooseOptionModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.Wallet1ScanPrimaryModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import dagger.Binds @@ -66,4 +67,9 @@ internal interface ModelModule { @IntoMap @ClassKey(Wallet1ChooseOptionModel::class) fun provideModel7(model: Wallet1ChooseOptionModel): Model + + @Binds + @IntoMap + @ClassKey(Wallet1ScanPrimaryModel::class) + fun provideModel8(model: Wallet1ScanPrimaryModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 0b74c9f64f..ad4786a6b7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -103,11 +103,19 @@ internal class OnboardingMultiWalletModel @Inject constructor( } private fun getInitialStep(): OnboardingMultiWalletState.Step { - val card = params.scanResponse.card + val scanResponse = params.scanResponse + val card = scanResponse.card val backupService = backupServiceHolder.backupService.get()!! return when { + // interrupted backup backupService.hasIncompletedBackup -> OnboardingMultiWalletState.Step.Finalize + + // Add backup button + // Wallet1 without backup and userwallet's scanResponse doesn't contain primary card. + card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup && + scanResponse.primaryCard == null -> OnboardingMultiWalletState.Step.ScanPrimary + card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> OnboardingMultiWalletState.Step.AddBackupDevice card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt index 294dd6049d..1906d6c2aa 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt @@ -17,6 +17,8 @@ data class OnboardingMultiWalletState( /** * Wallet1 + * ScanPrimary -> | + * | * CreateWallet -> ChooseBackupOption -> AddBackupDevice -> Finalize -> [Done] * | * -> [Done] @@ -27,7 +29,7 @@ data class OnboardingMultiWalletState( * -> AddBackupDevice -> Finalize -> [Done] */ enum class Step { - CreateWallet, ChooseBackupOption, SeedPhrase, AddBackupDevice, Finalize, Done + CreateWallet, ChooseBackupOption, SeedPhrase, ScanPrimary, AddBackupDevice, Finalize, Done } @JvmInline diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt index 42d6d6046d..7c1d7416ed 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt @@ -7,7 +7,9 @@ import com.tangem.features.onboarding.v2.impl.R fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = when (step) { OnboardingMultiWalletState.Step.CreateWallet -> resourceReference(R.string.onboarding_create_wallet_header) - OnboardingMultiWalletState.Step.AddBackupDevice -> + OnboardingMultiWalletState.Step.ScanPrimary, + OnboardingMultiWalletState.Step.AddBackupDevice, + -> resourceReference(R.string.onboarding_navbar_title_creating_backup) OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index 2c5b1af15f..ae225a9f48 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -49,9 +49,9 @@ internal fun OnboardingMultiWallet( url2 = state.artwork2Url, url3 = state.artwork3Url, modifier = Modifier - .weight(.56f) .padding(horizontal = 34.dp) .padding(top = 20.dp) + .weight(.4f) .fillMaxWidth(), state = artworksState, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt index eb6a9c353d..19cf83fac8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt @@ -3,11 +3,15 @@ package com.tangem.features.onboarding.v2.visa.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.TitleProvider interface OnboardingVisaComponent : ComposableContentComponent, InnerNavigationHolder { - data class Params(val titleProvider: TitleProvider) + data class Params( + val scanResponse: ScanResponse, + val titleProvider: TitleProvider, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index fe903926b8..67982e4535 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @Suppress("UnusedPrivateMember") -class DefaultOnboardingVisaComponent @AssistedInject constructor( +internal class DefaultOnboardingVisaComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: OnboardingVisaComponent.Params, ) : OnboardingVisaComponent, AppComponentContext by appComponentContext { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt index 2c3e0eb8dd..5ffff0e28f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt @@ -2,23 +2,42 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch -class OnboardingVisaAccessCodeComponent( +internal class OnboardingVisaAccessCodeComponent( appComponentContext: AppComponentContext, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(params) + private val model: OnboardingVisaAccessCodeModel = getOrCreateModel() + + init { + componentScope.launch { + model.onBack.collect { params.onBack() } + } + componentScope.launch { + params.parentBackEvent.collect { + model.onBack() + } + } + } @Composable override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + BackHandler(onBack = model::onBack) + + OnboardingVisaAccessCode(state, modifier) } data class Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 678c620c27..24d171ebb9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -3,22 +3,95 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM +import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ComponentScoped -class OnboardingVisaAccessCodeModel @Inject constructor( +internal class OnboardingVisaAccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val paramsContainer: ParamsContainer, + private val tangemSdkManager: TangemSdkManager, ) : Model() { - @Suppress("UnusedPrivateMember") - private val params = paramsContainer.require() + private val _uiState = MutableStateFlow(getInitialState()) + val uiState = _uiState.asStateFlow() + val onBack = MutableSharedFlow() fun onBack() { - TODO() + when (uiState.value.step) { + OnboardingVisaAccessCodeUM.Step.Enter -> modelScope.launch { onBack.emit(Unit) } + OnboardingVisaAccessCodeUM.Step.ReEnter -> + _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.Enter) } + } + } + + private fun getInitialState(): OnboardingVisaAccessCodeUM { + return OnboardingVisaAccessCodeUM( + onAccessCodeFirstChange = { _uiState.update { it.copy(accessCodeFirst = it.accessCodeFirst) } }, + onAccessCodeSecondChange = { _uiState.update { it.copy(accessCodeSecond = it.accessCodeSecond) } }, + onAccessCodeHideClick = { _uiState.update { it.copy(accessCodeHidden = !it.accessCodeHidden) } }, + onContinue = { onContinue() }, + ) + } + + private fun onContinue() { + if (checkAccessCode().not()) return + + when (uiState.value.step) { + OnboardingVisaAccessCodeUM.Step.Enter -> { + _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } + } + OnboardingVisaAccessCodeUM.Step.ReEnter -> { + startActivationProcess(accessCode = uiState.value.accessCodeFirst.text) + } + } + } + + private fun checkAccessCode(): Boolean { + val first = uiState.value.accessCodeFirst.text + val second = uiState.value.accessCodeSecond.text + + when { + first.length < ACCESS_CODE_MIN_LENGTH -> { + _uiState.update { it.copy(atLeastMinCharsError = true) } + return false + } + first != second -> { + _uiState.update { it.copy(codesNotMatchError = true) } + return false + } + } + + return true + } + + private fun startActivationProcess(accessCode: String) { + modelScope.launch { + @Suppress("UnusedPrivateMember") + val result = tangemSdkManager.activateVisaCard( + accessCode = accessCode, + challengeToSign = null, + activationInput = TODO(), + ) + + // when (result) { + // is CompletionResult.Success -> { + // val response = result.data + // TODO() + // } + // is CompletionResult.Failure -> { + // // show alert + // TODO() + // } + // } + } + } + + private companion object { + const val ACCESS_CODE_MIN_LENGTH = 4 } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt new file mode 100644 index 0000000000..f5c8364420 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -0,0 +1,167 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.components.OutlineTextFieldWithIcon +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM +import kotlinx.coroutines.delay + +@Composable +internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + AnimatedContent( + modifier = Modifier + .padding(top = 48.dp) + .weight(1f), + targetState = state.step, + transitionSpec = TangemAnimations.AnimatedContent + .slide { initial, target -> initial.ordinal < target.ordinal }, + ) { step -> + Content( + state = state, + reEnterAccessCodeState = step == OnboardingVisaAccessCodeUM.Step.ReEnter, + ) + } + + SpacerH(20.dp) + + NavigationPrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + primaryButton = NavigationButton( + textReference = when (state.step) { + OnboardingVisaAccessCodeUM.Step.Enter -> resourceReference(R.string.common_continue) + OnboardingVisaAccessCodeUM.Step.ReEnter -> + resourceReference(R.string.onboarding_create_wallet_button_create_wallet) + }, + onClick = state.onContinue, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = state.step == OnboardingVisaAccessCodeUM.Step.ReEnter, + ), + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun Content( + state: OnboardingVisaAccessCodeUM, + reEnterAccessCodeState: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .padding(horizontal = 32.dp), + text = if (reEnterAccessCodeState) { + "Re-enter your access code" // TODO + } else { + "Create access code" // TODO + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH16() + + // TODO + Text( + modifier = Modifier + .padding(horizontal = 32.dp), + text = "The access code will be used manage your payment account and protect it from unauthorized access", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + SpacerH32() + + val focusRequester = remember { FocusRequester() } + + OutlineTextFieldWithIcon( + modifier = modifier + .padding(horizontal = 16.dp) + .focusRequester(focusRequester) + .fillMaxWidth(), + iconResId = if (state.accessCodeHidden) { + R.drawable.ic_eye_outline_24 + } else { + R.drawable.ic_eye_off_outline_24 + }, + iconColor = TangemTheme.colors.icon.primary1, + onIconClick = state.onAccessCodeHideClick, + value = if (reEnterAccessCodeState) { + state.accessCodeSecond + } else { + state.accessCodeFirst + }, + onValueChange = if (reEnterAccessCodeState) { + state.onAccessCodeSecondChange + } else { + state.onAccessCodeFirstChange + }, + label = stringResource(id = R.string.onboarding_wallet_info_title_third), + isError = state.codesNotMatchError || state.atLeastMinCharsError, + visualTransformation = if (state.accessCodeHidden) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + }, + caption = when { + state.codesNotMatchError && reEnterAccessCodeState -> + stringResource(R.string.onboarding_access_codes_doesnt_match) + state.atLeastMinCharsError && !reEnterAccessCodeState -> + stringResource(R.string.onboarding_access_code_too_short) + else -> null + }, + ) + + LaunchedEffect(Unit) { + delay(timeMillis = 200) + focusRequester.requestFocus() + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaAccessCode( + state = OnboardingVisaAccessCodeUM(), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt new file mode 100644 index 0000000000..0cf7770ac0 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state + +import androidx.compose.ui.text.input.TextFieldValue + +data class OnboardingVisaAccessCodeUM( + val step: Step = Step.Enter, + val accessCodeFirst: TextFieldValue = TextFieldValue(""), + val accessCodeSecond: TextFieldValue = TextFieldValue(""), + val codesNotMatchError: Boolean = false, + val atLeastMinCharsError: Boolean = false, + val accessCodeHidden: Boolean = true, + val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {}, + val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {}, + val onContinue: () -> Unit = {}, + val onAccessCodeHideClick: () -> Unit = {}, +) { + enum class Step { + Enter, ReEnter + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt index 8dab8ddac4..0194489e69 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt @@ -6,8 +6,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.OnboardingVisaWelcome +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM -class OnboardingVisaWelcomeComponent( +internal class OnboardingVisaWelcomeComponent( appComponentContext: AppComponentContext, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -15,9 +17,15 @@ class OnboardingVisaWelcomeComponent( @Composable override fun Content(modifier: Modifier) { BackHandler(onBack = remember(this) { { params.onBack() } }) + + OnboardingVisaWelcome( + modifier = modifier, + state = OnboardingVisaWelcomeUM(), + ) } data class Params( val onBack: () -> Unit, + val startActivation: () -> Unit, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt new file mode 100644 index 0000000000..61c2b6e0d8 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -0,0 +1,124 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM + +@Composable +internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + Modifier + .padding( + top = 20.dp, + bottom = 32.dp, + start = 32.dp, + end = 32.dp, + ) + .fillMaxSize() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Artwork() + + SpacerH(50.dp) + + Text( + text = when (state.mode) { + OnboardingVisaWelcomeUM.Mode.Hello -> "Hello, ${state.userName}!" + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "Welcome back, ${state.userName}!" + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + SpacerH12() + + Text( + text = when (state.mode) { + OnboardingVisaWelcomeUM.Mode.Hello -> "bla bla bla //TODO" + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "bla bla bla? //TODO" + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) + } + + when (state.mode) { + OnboardingVisaWelcomeUM.Mode.Hello -> { + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = "Start Activation", + onClick = state.onContinueClick, + ) + } + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> { + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = "Start Activation", + iconResId = R.drawable.ic_tangem_24, + onClick = state.onContinueClick, + ) + } + } + } +} + +@Composable +fun Artwork(modifier: Modifier = Modifier) { + Box(modifier) { + val circleColor = TangemTheme.colors.background.secondary + + Canvas( + modifier = Modifier + .align(Alignment.Center) + .size(254.dp) + .fillMaxSize(), + ) { + drawCircle( + color = circleColor, + radius = size.width / 2, + center = center, + ) + } + + AsyncImage( + model = R.drawable.img_card_visa, + contentDescription = null, + modifier = Modifier + .align(Alignment.Center) + .size(128.dp), // Adjust the size as needed + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaWelcome(state = OnboardingVisaWelcomeUM()) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt new file mode 100644 index 0000000000..9e1f6ba81b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state + +internal data class OnboardingVisaWelcomeUM( + val mode: Mode = Mode.Hello, + val userName: String = "", + val onContinueClick: () -> Unit = {}, +) { + enum class Mode { + Hello, WelcomeBack + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..abb9a0cecf --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onboarding.v2.visa.impl.di + +import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultOnboardingVisaComponent.Factory): OnboardingVisaComponent.Factory +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt index 51e56a32da..bdeb4fe70e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState @@ -56,7 +56,7 @@ fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) { SecondaryButton( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_seed_intro_button_generate), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_generate), enabled = state.buttonGenerateSeedPhrase.enabled, showProgress = state.buttonGenerateSeedPhrase.showProgress, onClick = state.buttonGenerateSeedPhrase.onClick, @@ -66,7 +66,7 @@ fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) { SecondaryButton( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_seed_intro_button_import), + text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_import), enabled = state.buttonImportSeedPhrase.enabled, showProgress = state.buttonImportSeedPhrase.showProgress, onClick = state.buttonImportSeedPhrase.onClick, @@ -105,7 +105,7 @@ private fun EditIconBlock() { horizontal = TangemTheme.dimens.size12, vertical = TangemTheme.dimens.size4, ), - text = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy), + text = stringResourceSafe(id = R.string.onboarding_seed_phrase_intro_legacy), color = TangemTheme.colors.text.warning, style = TangemTheme.typography.body1, ) @@ -143,7 +143,7 @@ private fun ReadMoreBlock(state: AboutState) { ) SpacerW8() Text( - text = stringResource(id = R.string.onboarding_seed_button_read_more), + text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more), color = TangemTheme.colors.text.primary1, ) } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt index c98eb683e9..bf29010072 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt @@ -2,14 +2,22 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.R import com.tangem.core.ui.components.OutlineTextField import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState +import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock @@ -29,7 +37,6 @@ fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modi .weight(1f) .fillMaxWidth(), ) { - item { OnboardingDescriptionBlock( modifier = Modifier @@ -59,7 +66,7 @@ fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modi PrimaryButtonIconEnd( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), + text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet), iconResId = R.drawable.ic_tangem_24, enabled = state.buttonCreateWallet.enabled, showProgress = state.buttonCreateWallet.showProgress, @@ -73,25 +80,31 @@ fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modi @Composable private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) { Column(modifier) { - OutlineTextField( - value = state.tvSecondPhrase.textFieldValue, - onValueChange = state.tvSecondPhrase.onTextFieldValueChanged, - label = state.tvSecondPhrase.label, - isError = state.tvSecondPhrase.isError, - ) + CheckSeedPhraseTextField(state = state.tvSecondPhrase, imeAction = ImeAction.Next) + CheckSeedPhraseTextField(state = state.tvSeventhPhrase, imeAction = ImeAction.Next) + CheckSeedPhraseTextField(state = state.tvEleventhPhrase, imeAction = ImeAction.Done) + } +} - OutlineTextField( - value = state.tvSeventhPhrase.textFieldValue, - onValueChange = state.tvSeventhPhrase.onTextFieldValueChanged, - label = state.tvSeventhPhrase.label, - isError = state.tvSeventhPhrase.isError, - ) +@Composable +private fun CheckSeedPhraseTextField(state: TextFieldState, imeAction: ImeAction) { + val focusManager = LocalFocusManager.current + DisableContextMenu { OutlineTextField( - value = state.tvEleventhPhrase.textFieldValue, - onValueChange = state.tvEleventhPhrase.onTextFieldValueChanged, - label = state.tvEleventhPhrase.label, - isError = state.tvEleventhPhrase.isError, + value = state.textFieldValue, + onValueChange = state.onTextFieldValueChanged, + label = state.label, + isError = state.isError, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = imeAction, + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() }, + onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) }, + ), ) } } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index de2ffddcbc..bc6e59c4dd 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -14,16 +15,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import com.tangem.core.ui.components.* -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState @@ -49,7 +51,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo OnboardingDescriptionBlock( modifier = Modifier.padding(top = TangemTheme.dimens.size16), ) { - DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message)) + DescriptionSubTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_import_message)) } PhraseBlock( modifier = Modifier @@ -70,7 +72,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo PrimaryButtonIconEnd( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.common_import), + text = stringResourceSafe(id = R.string.common_import), iconResId = R.drawable.ic_tangem_24, enabled = state.buttonCreateWallet.enabled, showProgress = state.buttonCreateWallet.showProgress, @@ -108,9 +110,13 @@ private fun PassphraseBlock( onValueChange = passphraseField.onTextFieldValueChanged, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, - label = stringResource(id = R.string.common_passphrase), - placeholder = stringResource(id = R.string.send_optional_field), + label = stringResourceSafe(id = R.string.common_passphrase), + placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = onPassphraseInfoClick, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + ), ) } @@ -135,6 +141,10 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif style = SpanStyle(color = TangemTheme.colors.text.warning), ), colors = TangemTextFieldsDefault.defaultTextFieldColors, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + ), ) Box( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt index 62a4ffde12..5691a1b124 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt @@ -8,11 +8,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState @@ -49,7 +49,7 @@ fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) { PrimaryButtonIconEnd( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), + text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet), iconResId = R.drawable.ic_tangem_24, enabled = state.buttonCreateWallet.enabled, showProgress = state.buttonCreateWallet.showProgress, @@ -60,7 +60,7 @@ fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) { SecondaryButton( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_options_button_options), + text = stringResourceSafe(id = R.string.onboarding_create_wallet_options_button_options), enabled = state.buttonOtherOptions.enabled, showProgress = state.buttonOtherOptions.showProgress, onClick = state.buttonOtherOptions.onClick, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt index 9228432a75..cf5f16d4d0 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt @@ -11,12 +11,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R @@ -50,7 +50,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte ) Text( - text = stringResource(id = R.string.common_passphrase), + text = stringResourceSafe(id = R.string.common_passphrase), modifier = Modifier .padding(top = TangemTheme.dimens.size40) .align(Alignment.CenterHorizontally), @@ -59,7 +59,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte ) Text( - text = stringResource(id = R.string.onboarding_bottom_sheet_passphrase_description), + text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), modifier = Modifier .padding(top = TangemTheme.dimens.size16) .padding(horizontal = TangemTheme.dimens.size24) @@ -75,7 +75,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte .padding(top = TangemTheme.dimens.size40) .padding(bottom = TangemTheme.dimens.size32) .fillMaxWidth(), - text = stringResource(id = R.string.common_ok), + text = stringResourceSafe(id = R.string.common_ok), onClick = content.onOkClick, ) } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 2d767dc78a..e5d110febd 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -9,13 +9,13 @@ 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.pluralStringResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.pluralStringResourceSafe +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R @@ -60,7 +60,7 @@ fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifi firstActionContent = { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.common_continue), + text = stringResourceSafe(id = R.string.common_continue), onClick = state.buttonContinue.onClick, ) }, @@ -77,10 +77,10 @@ private fun TitleBlock(state: SegmentSeedState) { .fillMaxWidth(), ) { Column { - DescriptionTitleText(text = stringResource(id = R.string.onboarding_seed_generate_title)) + DescriptionTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_generate_title)) SpacerH16() DescriptionSubTitleText( - text = pluralStringResource( + text = pluralStringResourceSafe( id = R.plurals.onboarding_seed_generate_message_words_count, count = state.selectedSeedType.count, state.selectedSeedType.count, @@ -101,7 +101,7 @@ private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modif }, ) { Text( - text = pluralStringResource( + text = pluralStringResourceSafe( id = R.plurals.onboarding_seed_generate_words_count, count = it.count, it.count, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt index 5dc8a2a1c0..e0ee2d086e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt @@ -8,10 +8,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme /** @@ -31,9 +31,9 @@ fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composab @Composable fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) { Column { - DescriptionTitleText(text = stringResource(id = titleRes)) + DescriptionTitleText(text = stringResourceSafe(id = titleRes)) SpacerH16() - DescriptionSubTitleText(text = stringResource(id = subTitleRes)) + DescriptionSubTitleText(text = stringResourceSafe(id = subTitleRes)) } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt index 56169d7fa1..5562ff3af5 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt @@ -69,7 +69,7 @@ class ImportSeedPhraseStateBuilder { return uiState.copy( importSeedPhraseState = uiState.importSeedPhraseState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = ShowPassphraseInfoBottomSheetContent(onDismiss), ), @@ -81,7 +81,7 @@ class ImportSeedPhraseStateBuilder { return uiState.copy( importSeedPhraseState = uiState.importSeedPhraseState.copy( bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy( - isShow = false, + isShown = false, ), ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt index 0a88231cc3..0e1ea2888c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -47,7 +47,7 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor( val bottomSheet by bottomSheetSlot.subscribeAsState() val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) @@ -70,12 +70,14 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor( context = childByContext(componentContext), params = SelectCountryComponent.Params( cryptoCurrency = params.cryptoCurrency, - onDismiss = { + onDismiss = { isCountrySelected -> // Dismiss country select sheet model.bottomSheetNavigation.dismiss() - // Dismiss confirm residency sheet - config.onDismiss() + if (isCountrySelected) { + // Dismiss confirm residency sheet + config.onDismiss() + } }, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt index adc039dca9..70eecdc9a2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt @@ -33,7 +33,7 @@ internal class PreviewConfirmResidencyComponent( val state by previewState.collectAsStateWithLifecycle() val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt index d91f5881bb..4fedef0994 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -23,6 +22,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent @@ -55,7 +55,7 @@ internal fun ConfirmResidencyBottomSheetContent(model: ConfirmResidencyUM, modif modifier = Modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing10), - text = stringResource(id = R.string.onramp_residency_bottomsheet_title), + text = stringResourceSafe(id = R.string.onramp_residency_bottomsheet_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -114,7 +114,7 @@ private fun CountryContent(name: String, flagUrl: String, isCountrySupported: Bo if (!isCountrySupported) { Text( modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), - text = stringResource(id = R.string.onramp_residency_bottomsheet_country_not_supported), + text = stringResourceSafe(id = R.string.onramp_residency_bottomsheet_country_not_supported), color = TangemTheme.colors.text.warning, style = TangemTheme.typography.body2, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 18a7bb1727..351353cfe7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -30,6 +30,7 @@ internal class OnrampStateFactory( currency = currency, onClose = onClose, openSettings = onrampIntents::openSettings, + errorNotification = null, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 2a443efd36..b9a666cb86 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -47,6 +47,7 @@ internal class OnrampAmountStateFactory( precision = currency.precision, ), amountFieldModel = amountState.amountFieldModel.copy( + isError = false, fiatAmount = amountState.amountFieldModel.fiatAmount.copy( currencySymbol = currency.unit, decimals = currency.precision, @@ -67,6 +68,7 @@ internal class OnrampAmountStateFactory( amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), providerBlockState = OnrampProviderBlockUM.Loading, buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false), + errorNotification = null, ) } @@ -97,6 +99,7 @@ internal class OnrampAmountStateFactory( } }, ), + errorNotification = null, ) } @@ -172,6 +175,7 @@ internal class OnrampAmountStateFactory( } }, ), + errorNotification = null, ) } @@ -189,6 +193,7 @@ internal class OnrampAmountStateFactory( amount = TextReference.EMPTY, ), ), + errorNotification = null, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index f53b13e685..63a9b5ce7e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -1,7 +1,6 @@ package com.tangem.features.onramp.main.model import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.res.stringResource import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.ui.alerts.models.AlertDemoModeUM @@ -11,10 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent @@ -23,7 +20,6 @@ import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory @@ -346,31 +342,19 @@ internal class OnrampMainComponentModel @Inject constructor( private fun showDemoWarning() { val alertUM = AlertDemoModeUM(onConfirmClick = {}) - - messageSender.send( - message = ContentMessage { onDismiss -> - val confirmButton = DialogButtonUM( - title = alertUM.confirmButtonText.resolveReference(), - onClick = { - alertUM.onConfirmClick() - onDismiss() - }, - ) - - val dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), - onClick = onDismiss, - ) - - BasicDialog( - message = alertUM.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = alertUM.title.resolveReference(), - dismissButton = dismissButton, + val message = DialogMessage( + title = alertUM.title, + message = alertUM.message, + firstActionBuilder = { + EventMessageAction( + title = alertUM.confirmButtonText, + onClick = alertUM.onConfirmClick, ) }, + secondActionBuilder = { cancelAction() }, ) + + messageSender.send(message) } private fun sendOnrampErrorAnalytic(error: OnrampError) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index 381a77989d..e13fd96310 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -13,11 +13,11 @@ 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.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import com.tangem.core.ui.extensions.appendSpace +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM @@ -47,7 +47,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Column(modifier = Modifier.weight(1F)) { Text( text = buildAnnotatedString { - append(stringResource(id = R.string.onramp_pay_with)) + append(stringResourceSafe(id = R.string.onramp_pay_with)) appendSpace() withStyle( style = SpanStyle( @@ -63,7 +63,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: ) Text( text = buildAnnotatedString { - append(stringResource(id = R.string.onramp_via)) + append(stringResourceSafe(id = R.string.onramp_via)) appendSpace() append(state.providerName) }, @@ -87,7 +87,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: horizontal = TangemTheme.dimens.spacing6, vertical = TangemTheme.dimens.spacing1, ), - text = stringResource(id = R.string.express_provider_best_rate), + text = stringResourceSafe(id = R.string.express_provider_best_rate), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.primary2, ) @@ -105,7 +105,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = stringResource(id = R.string.express_provider), + text = stringResourceSafe(id = R.string.express_provider), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -119,7 +119,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { modifier = Modifier.size(TangemTheme.dimens.size16), ) Text( - text = stringResource(id = R.string.express_fetch_best_rates), + text = stringResourceSafe(id = R.string.express_fetch_best_rates), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt index 75a8748835..25d8b7cb79 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt @@ -24,7 +24,7 @@ internal class DefaultSelectPaymentMethodComponent @AssistedInject constructor( override fun BottomSheet() { val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = PaymentMethodsBottomSheetConfig( selectedMethodId = params.selectedMethodId, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt index e59e4722e9..d0bcd4a3d0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt @@ -52,7 +52,7 @@ internal class DefaultSelectProviderComponent @AssistedInject constructor( val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index e85759d361..bc077ed6a3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -76,10 +76,13 @@ internal class SelectProviderModel @Inject constructor( val filteredEmptyMethods = methods.mapNotNull { method -> val providers = getOnrampProviderWithQuoteUseCase(method).getOrNull() if (!providers.isNullOrEmpty()) { + val allErrorProviders = providers.all { + it is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod + } PaymentProviderUM( paymentMethod = method, providers = providers.toProvidersListItems(), - ) + ).takeIf { !allErrorProviders } } else { null } @@ -225,7 +228,7 @@ internal class SelectProviderModel @Inject constructor( }, ) } - is OnrampProviderWithQuote.Unavailable.Error -> { + is OnrampProviderWithQuote.Unavailable.AmountError -> { val quoteError = quote.quoteError val amount = quoteError.error.requiredAmount.format { crypto(symbol = quoteError.fromAmount.symbol, decimals = quoteError.fromAmount.decimals) @@ -307,7 +310,7 @@ internal class SelectProviderModel @Inject constructor( is OnrampProviderWithQuote.Data -> it.toAmount.value // negative difference to sort both when data and unavailable is present - is OnrampProviderWithQuote.Unavailable.Error -> { + is OnrampProviderWithQuote.Unavailable.AmountError -> { when (val error = it.quoteError.error) { is OnrampError.AmountError.TooSmallError -> it.quoteError.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.quoteError.fromAmount.value diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 071e09b4b6..39cc85083b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -33,6 +32,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.onramp.model.OnrampPaymentMethod @@ -61,7 +61,7 @@ internal fun SelectProviderBottomSheetContent(state: SelectPaymentAndProviderUM, horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(id = R.string.onramp_choose_provider_title_hint), + text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -102,7 +102,7 @@ private fun PaymentMethodBlock( PaymentMethodIcon(imageUrl = state.imageUrl) Column(modifier = Modifier.weight(1F)) { Text( - text = stringResource(id = R.string.onramp_pay_with), + text = stringResourceSafe(id = R.string.onramp_pay_with), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) @@ -193,7 +193,7 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m when { state.isBestRate -> { Text( - text = stringResource(R.string.express_provider_best_rate), + text = stringResourceSafe(R.string.express_provider_best_rate), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.constantWhite, modifier = Modifier @@ -278,7 +278,7 @@ private fun OnrampMoreProviders() { .padding(top = 16.dp), ) Text( - text = stringResource(R.string.express_more_providers_soon), + text = stringResourceSafe(R.string.express_more_providers_soon), style = TangemTheme.typography.caption2, color = TangemTheme.colors.icon.informative, modifier = Modifier diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index 91b94beb8e..cc6426d771 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -1,20 +1,17 @@ package com.tangem.features.onramp.redirect.model -import androidx.compose.ui.res.stringResource import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.features.onramp.impl.R @@ -86,22 +83,11 @@ internal class OnrampRedirectModel @Inject constructor( providerName = params.onrampProviderWithQuote.provider.info.name, paymentMethod = params.onrampProviderWithQuote.paymentMethod.name, ) - val contentMessage = ContentMessage { onDismiss -> - BasicDialog( - message = stringResource(id = R.string.common_unknown_error), - confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), - onClick = { - params.onBack() - onDismiss() - }, - ), - onDismissDialog = { - params.onBack() - onDismiss() - }, - ) - } - messageSender.send(contentMessage) + val message = DialogMessage( + message = resourceReference(R.string.common_unknown_error), + onDismissRequest = params.onBack, + ) + + messageSender.send(message) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt index 35fc452dc8..0cc50766e3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt @@ -25,7 +25,7 @@ internal class DefaultSelectCountryComponent @AssistedInject constructor( private val model: OnrampSelectCountryModel = getOrCreateModel(params) override fun dismiss() { - model.dismiss() + model.dismiss(isCountrySelected = false) } @Composable @@ -33,7 +33,7 @@ internal class DefaultSelectCountryComponent @AssistedInject constructor( val state by model.state.collectAsStateWithLifecycle() val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt index ba72314dd6..8abf849546 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency internal interface SelectCountryComponent : ComposableBottomSheetComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: () -> Unit) + data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: (Boolean) -> Unit) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index d7e2a1c6ad..94296a59e1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -60,8 +60,8 @@ internal class OnrampSelectCountryModel @Inject constructor( modelScope.launch { subscribeOnUpdateState() } } - fun dismiss() { - params.onDismiss() + fun dismiss(isCountrySelected: Boolean) { + params.onDismiss(isCountrySelected) } private suspend fun subscribeOnUpdateState() { @@ -92,7 +92,7 @@ internal class OnrampSelectCountryModel @Inject constructor( analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChosen(country.name)) modelScope.launch { saveDefaultCountryUseCase.invoke(country) - dismiss() + dismiss(isCountrySelected = true) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index b74c50c6b1..224e1c4168 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer @@ -24,6 +23,7 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcountry.entity.CountryItemState @@ -67,7 +67,7 @@ private fun LazyListScope.countryListError(state: CountryListUM.Error) { verticalArrangement = Arrangement.Center, ) { Text( - text = stringResource(id = R.string.markets_loading_error_title), + text = stringResourceSafe(id = R.string.markets_loading_error_title), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) @@ -185,7 +185,7 @@ private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailab color = TangemTheme.colors.text.disabled, ) Text( - text = stringResource(R.string.onramp_country_unavailable), + text = stringResourceSafe(R.string.onramp_country_unavailable), maxLines = 1, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt index 687df5d015..552c4023d0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt @@ -33,7 +33,7 @@ internal class DefaultSelectCurrencyComponent @AssistedInject constructor( val state by model.state.collectAsStateWithLifecycle() val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt index 8177c32631..472d72efcb 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -11,7 +11,6 @@ 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.res.stringResource import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer @@ -24,6 +23,7 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM @@ -67,7 +67,7 @@ private fun LazyListScope.currencyListError(state: CurrenciesListUM.Error) { verticalArrangement = Arrangement.Center, ) { Text( - text = stringResource(id = R.string.markets_loading_error_title), + text = stringResourceSafe(id = R.string.markets_loading_error_title), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 79bcd5e533..77168538c5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -1,6 +1,5 @@ package com.tangem.features.onramp.selecttoken.model -import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.common.routing.AppRouter import com.tangem.common.ui.alerts.models.AlertDemoModeUM @@ -11,10 +10,8 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase @@ -130,30 +127,19 @@ internal class OnrampOperationModel @Inject constructor( if (isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { val alertUM = AlertDemoModeUM(onConfirmClick = {}) - messageSender.send( - message = ContentMessage { onDismiss -> - val confirmButton = DialogButtonUM( - title = alertUM.confirmButtonText.resolveReference(), - onClick = { - alertUM.onConfirmClick() - onDismiss() - }, - ) - - val dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), - onClick = onDismiss, - ) - - BasicDialog( - message = alertUM.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = alertUM.title.resolveReference(), - dismissButton = dismissButton, + val message = DialogMessage( + title = alertUM.title, + message = alertUM.message, + firstActionBuilder = { + EventMessageAction( + title = alertUM.confirmButtonText, + onClick = alertUM.onConfirmClick, ) }, + secondActionBuilder = { cancelAction() }, ) + + messageSender.send(message) } else { action() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index efbc6b5cfa..0347a146bd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -10,9 +10,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.impl.R @@ -39,7 +39,7 @@ internal fun OnrampSelectToken( stickyHeader(key = "header") { AppBarWithBackButton( onBackClick = state.onBackClick, - text = stringResource(id = state.titleResId), + text = stringResourceSafe(id = state.titleResId), iconRes = R.drawable.ic_close_24, containerColor = TangemTheme.colors.background.secondary, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt index 36999376a3..a3c7234332 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt @@ -53,7 +53,7 @@ internal class DefaultOnrampSettingsComponent @AssistedInject constructor( context = childByContext(componentContext), params = SelectCountryComponent.Params( params.cryptoCurrency, - model.bottomSheetNavigation::dismiss, + onDismiss = { model.bottomSheetNavigation.dismiss() }, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt index 949a3af30e..8a0d062131 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.impl.R @@ -35,7 +35,7 @@ internal fun OnrampSettingsContent(state: OnrampSettingsUM, modifier: Modifier = AppBarWithBackButton( modifier = Modifier.statusBarsPadding(), onBackClick = state.onBack, - text = stringResource(id = R.string.onramp_settings_title), + text = stringResourceSafe(id = R.string.onramp_settings_title), iconRes = R.drawable.ic_close_24, ) }, @@ -81,7 +81,7 @@ private fun ResidenceSection(state: OnrampSettingsItemUM.Residence, modifier: Mo verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringResource(id = R.string.onramp_settings_residence), + text = stringResourceSafe(id = R.string.onramp_settings_residence), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) @@ -103,7 +103,7 @@ private fun ResidenceSection(state: OnrampSettingsItemUM.Residence, modifier: Mo ) } Text( - text = stringResource(id = R.string.onramp_settings_residence_description), + text = stringResourceSafe(id = R.string.onramp_settings_residence_description), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index 435ced42ba..834e4fb850 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -52,22 +52,27 @@ internal class SetOnrampSuccessContentConverter( } private fun getNotification(status: OnrampStatus.Status, externalTxUrl: String?): NotificationUM? { - if (externalTxUrl == null) return null return when (status) { OnrampStatus.Status.Verifying -> { - ExpressNotificationsUM.NeedVerification { - goToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.NeedVerification( + onGoToProviderClick = onProviderClick(externalTxUrl), + ) } OnrampStatus.Status.Failed -> { - ExpressNotificationsUM.FailedByProvider { - goToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.FailedByProvider( + onGoToProviderClick = onProviderClick(externalTxUrl), + ) } else -> null } } + private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { + { goToProviderClick(externalTxUrl) } + } else { + null + } + private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM { val statuses = with(status) { persistentListOf( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 514a00dc19..34f4ff9f39 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -1,6 +1,5 @@ package com.tangem.features.onramp.success.model -import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model @@ -8,10 +7,9 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.GetOnrampTransactionUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase @@ -81,10 +79,17 @@ internal class OnrampSuccessComponentModel @Inject constructor( } private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { - val cryptoCurrency = getCryptoCurrencyUseCase( + val multiCryptoCurrency = getCryptoCurrencyUseCase( transaction.userWalletId, transaction.toCurrencyId, - ).getOrElse { error("Crypto currency not found") } + ).getOrNull() + + val cryptoCurrency = multiCryptoCurrency + ?: getCryptoCurrencyUseCase.invoke(transaction.userWalletId).getOrElse { + Timber.e("Crypto currency not found") + showErrorAlert(OnrampError.DomainError(null)) + return + } getOnrampStatusUseCase(txId = transaction.txId) .fold( @@ -126,29 +131,17 @@ internal class OnrampSuccessComponentModel @Inject constructor( } private fun showErrorAlert(error: OnrampError) { - val contentMessage = ContentMessage { onDismiss -> - val errorCode = (error as? OnrampError.DataError)?.code - val message = if (errorCode.isNullOrBlank()) { - stringResource(R.string.common_unknown_error) + val errorCode = (error as? OnrampError.DataError)?.code + val message = DialogMessage( + message = if (errorCode.isNullOrBlank()) { + resourceReference(R.string.common_unknown_error) } else { - stringResource(R.string.express_error_code, wrappedList(errorCode)) - } - BasicDialog( - message = message, - confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), - onClick = { - router.pop() - onDismiss() - }, - ), - onDismissDialog = { - router.pop() - onDismiss() - }, - ) - } - messageSender.send(contentMessage) + resourceReference(R.string.express_error_code, wrappedList(errorCode)) + }, + onDismissRequest = router::pop, + ) + + messageSender.send(message) } private fun removeTransactionIfTerminalStatus( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt index e021c732e7..39ea071d73 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -27,10 +26,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters @@ -73,7 +69,7 @@ private fun Content(state: OnrampSuccessComponentUM.Content, onBackClick: () -> }, bottomBar = { PrimaryButton( - text = stringResource(R.string.common_close), + text = stringResourceSafe(R.string.common_close), onClick = onBackClick, modifier = Modifier .fillMaxWidth() @@ -171,7 +167,7 @@ private fun AmountProviderBlock(providerName: TextReference, providerImageUrl: S horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = stringResource(R.string.common_with), + text = stringResourceSafe(R.string.common_with), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt index bd45bcea37..da3b5ead84 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,10 +24,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.dashedBorder @@ -98,7 +94,7 @@ private fun RemoveButton(state: ExchangeCardUM.RemoveButtonUM?) { state ?: return@AnimatedVisibility Text( - text = stringResource(id = R.string.manage_tokens_remove), + text = stringResourceSafe(id = R.string.manage_tokens_remove), modifier = Modifier.clickable( indication = ripple(bounded = false), interactionSource = remember { MutableInteractionSource() }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt index 7208284962..bf873aa6f5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -13,9 +13,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.impl.R @@ -59,7 +59,7 @@ internal fun SwapSelectTokens( stickyHeader(key = "header") { AppBarWithBackButton( onBackClick = state.onBackClick, - text = stringResource(id = R.string.common_swap), + text = stringResourceSafe(id = R.string.common_swap), iconRes = R.drawable.ic_close_24, containerColor = TangemTheme.colors.background.secondary, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 2913ce4223..2deda8e910 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.onramp.tokenlist.entity.transformer +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM @@ -19,6 +20,7 @@ internal class UpdateTokenItemsTransformer( private val statuses: Map>, private val isBalanceHidden: Boolean, private val unavailableTokensHeaderReference: TextReference, + private val warning: NotificationUM? = null, ) : TokenListUMTransformer { override fun transform(prevState: TokenListUM): TokenListUM { @@ -38,7 +40,7 @@ internal class UpdateTokenItemsTransformer( ), unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference), isBalanceHidden = isBalanceHidden, - warning = null, + warning = warning, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 6568ab9e04..6fff2879e7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -1,17 +1,23 @@ package com.tangem.features.onramp.tokenlist.model import arrow.core.getOrElse +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -25,6 +31,7 @@ import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.isZero import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -43,6 +50,7 @@ internal class OnrampTokenListModel @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val rampStateManager: RampStateManager, + private val getUserCountryUseCase: GetUserCountryUseCase, ) : Model() { val state: StateFlow = tokenListUMController.state @@ -69,7 +77,8 @@ internal class OnrampTokenListModel @Inject constructor( flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), flow4 = searchManager.query, - ) { maybeTokenList, appCurrency, isBalanceHidden, query -> + flow5 = hasRestrictionForSellFlow(), + ) { maybeTokenList, appCurrency, isBalanceHidden, query, hasRestrictionForSell -> val currencies = maybeTokenList.getOrElse( ifLoading = { it ?: TokenList.Empty }, ifError = { TokenList.Empty }, @@ -90,16 +99,29 @@ internal class OnrampTokenListModel @Inject constructor( .let(::resourceReference), ) } else { + val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) { + maybeTokenList.isInsufficientBalanceForSell() + } else { + false + } + UpdateTokenItemsTransformer( appCurrency = appCurrency, onItemClick = params.onTokenClick, - statuses = filterByQueryTokenList.filterByAvailability(), + statuses = filterByQueryTokenList.let { + if (hasRestrictionForSell || isInsufficientBalanceForSell) { + mapOf(false to it) + } else { + it.filterByAvailability() + } + }, isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = when (params.filterOperation) { - OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header - OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header - OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header - }.let(::resourceReference), + unavailableTokensHeaderReference = getUnavailableTokensHeaderReference(), + warning = when { + hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction + isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling + else -> null + }, ) } } @@ -108,6 +130,32 @@ internal class OnrampTokenListModel @Inject constructor( .launchIn(modelScope) } + private fun hasRestrictionForSellFlow(): Flow { + return if (params.filterOperation == OnrampOperation.SELL) { + getUserCountryUseCase().map { maybe -> + maybe.isRight { country -> country is UserCountry.Russia } + } + } else { + flowOf(false) + } + } + + private fun Lce.isInsufficientBalanceForSell(): Boolean { + return if (params.filterOperation == OnrampOperation.SELL) { + isContent { + (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() ?: false + } + } else { + false + } + } + + private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) { + OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header + OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header + OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header + }.let(::resourceReference) + private fun onSearchQueryChange(newQuery: String) { val searchBar = state.value.searchBarUM if (searchBar.query == newQuery) return @@ -168,7 +216,10 @@ internal class OnrampTokenListModel @Inject constructor( ) } OnrampOperation.SELL -> { - rampStateManager.availableForSell(userWalletId = params.userWalletId, status = status) + rampStateManager.availableForSell( + userWalletId = params.userWalletId, + status = status, + ).isRight() } OnrampOperation.SWAP -> { val isAvailable = rampStateManager.availableForSwap( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 069b27e874..679abe3866 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.tokenlist.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -42,14 +43,18 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { if (state.warning == null) { SearchBar(searchBarUM = state.searchBarUM) } else { - when (state.warning) { - is NotificationUM.Warning.OnrampErrorNotification -> { - Notification(config = state.warning.config, containerColor = TangemTheme.colors.background.primary) + AnimatedContent(targetState = state.warning, label = "") { warning -> + when (warning) { + is NotificationUM.Warning.OnrampErrorNotification -> { + Notification( + config = warning.config, + containerColor = TangemTheme.colors.background.primary, + ) + } + else -> { + Notification(config = warning.config, containerColor = TangemTheme.colors.button.disabled) + } } - is NotificationUM.Warning.SwapNoAvailablePair -> { - Notification(config = state.warning.config, containerColor = TangemTheme.colors.button.disabled) - } - else -> Unit } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 5118630639..787c7bdfe9 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -25,6 +25,8 @@ import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.IOException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import javax.inject.Inject @@ -62,8 +64,15 @@ internal class QrScanningFragment : ComposeFragment() { private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { val selectedImage = it ?: Uri.EMPTY if (selectedImage != Uri.EMPTY) { - val image = InputImage.fromFilePath(requireContext(), selectedImage) - analyzer.analyze(image) + val mimeType = requireContext().contentResolver.getType(selectedImage) + if (mimeType.isImageMimeType()) { + try { + val image = InputImage.fromFilePath(requireContext(), selectedImage) + analyzer.analyze(image) + } catch (e: IOException) { + Timber.e(e, "Unable to get image $selectedImage from gallery") + } + } } } @@ -80,7 +89,7 @@ internal class QrScanningFragment : ComposeFragment() { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.launchGalleryEvent .collect { - galleryLauncher.launch(it.imageFilter) + galleryLauncher.launch(GALLERY_IMAGE_FILTER) delay(timeMillis = 2000) } } @@ -137,8 +146,13 @@ internal class QrScanningFragment : ComposeFragment() { } } + private fun String?.isImageMimeType() = this?.startsWith(prefix = "$IMAGE_MIME_TYPE/") == true + companion object { + private const val IMAGE_MIME_TYPE = "image" + private const val GALLERY_IMAGE_FILTER = "$IMAGE_MIME_TYPE/*" + fun create() = QrScanningFragment() } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt index 3d23eb1813..22e7790a45 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt @@ -3,6 +3,7 @@ package com.tangem.feature.qrscanning.presentation import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent data class CameraDeniedBottomSheetConfig( + val onSettingsClick: () -> Unit, val onGalleryClick: () -> Unit, val onCancelClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index cb06aa540c..dd1f555379 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -1,9 +1,6 @@ package com.tangem.feature.qrscanning.presentation -import android.content.Intent import android.content.res.Configuration -import android.net.Uri -import android.provider.Settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -11,13 +8,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.core.content.ContextCompat import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.qrscanning.impl.R @@ -36,26 +31,19 @@ fun CameraDeniedBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { - val context = LocalContext.current Column { SimpleSettingsRow( - title = stringResource(id = R.string.qr_scanner_camera_denied_settings_button), + title = stringResourceSafe(id = R.string.qr_scanner_camera_denied_settings_button), icon = R.drawable.ic_settings_24, - onItemsClick = { - val intent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", context.packageName, null), - ) - ContextCompat.startActivity(context, intent, null) - }, + onItemsClick = content.onSettingsClick, ) SimpleSettingsRow( - title = stringResource(id = R.string.qr_scanner_camera_denied_gallery_button), + title = stringResourceSafe(id = R.string.qr_scanner_camera_denied_gallery_button), icon = R.drawable.ic_gallery_24, onItemsClick = content.onGalleryClick, ) SimpleSettingsRow( - title = stringResource(id = R.string.common_close), + title = stringResourceSafe(id = R.string.common_close), icon = R.drawable.ic_close_24, onItemsClick = content.onCancelClick, modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), @@ -75,12 +63,12 @@ private fun CameraDeniedBottomSheetHeader(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), ) { Text( - text = stringResource(id = R.string.qr_scanner_camera_denied_title), + text = stringResourceSafe(id = R.string.qr_scanner_camera_denied_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) Text( - text = stringResource(id = R.string.qr_scanner_camera_denied_text), + text = stringResourceSafe(id = R.string.qr_scanner_camera_denied_text), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) @@ -95,9 +83,10 @@ private fun Preview_CameraDeniedBottomSheet() { TangemThemePreview { CameraDeniedBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = CameraDeniedBottomSheetConfig( + onSettingsClick = {}, onCancelClick = {}, onGalleryClick = {}, ), diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt index 5677a023f8..5dedeffebc 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt @@ -12,9 +12,10 @@ internal class ShowCameraDeniedBottomSheetTransformer( override fun transform(prevState: QrScanningState): QrScanningState { return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = clickIntents::onBackClick, content = CameraDeniedBottomSheetConfig( + onSettingsClick = clickIntents::onSettingsClick, onCancelClick = clickIntents::onBackClick, onGalleryClick = clickIntents::onGalleryClicked, ), diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt index 5c2ab95304..adfaf8bcff 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.qrscanning.viewmodel +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.presentation.QrScanningStateController import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer @@ -11,32 +12,30 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import javax.inject.Inject -interface QrScanningClickIntents { +internal interface QrScanningClickIntents { - val launchGallery: SharedFlow + val launchGallery: SharedFlow fun onBackClick() fun onQrScanned(qrCode: String) fun onGalleryClicked() -} -@JvmInline -value class GalleryRequest( - val imageFilter: String, -) + fun onSettingsClick() +} @ViewModelScoped internal class QrScanningClickIntentsImplementor @Inject constructor( private val stateHolder: QrScanningStateController, private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, + private val settingsManager: SettingsManager, private val dispatcher: CoroutineDispatcherProvider, ) : BaseQrScanningClickIntents(), QrScanningClickIntents { private var isScanned = false - override val launchGallery = MutableSharedFlow( + override val launchGallery = MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST, ) @@ -56,13 +55,13 @@ internal class QrScanningClickIntentsImplementor @Inject constructor( } override fun onGalleryClicked() { - launchGallery.tryEmit(GalleryRequest(imageFilter = GALLERY_IMAGE_FILTER)) + launchGallery.tryEmit(Unit) if (stateHolder.value.bottomSheetConfig != null) { stateHolder.update(DismissBottomSheetTransformer()) } } - companion object { - private const val GALLERY_IMAGE_FILTER = "image/*" + override fun onSettingsClick() { + settingsManager.openAppSettings() } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index a504e00e9f..ceff5566d6 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -31,7 +31,7 @@ internal class QrScanningViewModel @Inject constructor( private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY] val uiState: StateFlow = stateHolder.uiState - val launchGalleryEvent: SharedFlow = clickIntents.launchGallery + val launchGalleryEvent: SharedFlow = clickIntents.launchGallery init { // samsung for some reason disables reader mode, and then it works unstable diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 93d3ad159f..63f2a5e291 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { /** Core modules */ implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.navigation) implementation(projects.core.res) implementation(projects.core.utils) implementation(projects.core.ui) @@ -37,7 +38,6 @@ dependencies { /** Other libraries */ implementation(deps.compose.shimmer) - implementation(deps.compose.accompanist.webView) implementation(deps.compose.accompanist.systemUiController) /** DI */ diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index dc7b06e5ef..2029309da1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -50,6 +50,6 @@ internal data class ReferralStateHolder( data class Analytics( val onAgreementClicked: () -> Unit, val onCopyClicked: () -> Unit, - val onShareClicked: () -> Unit, + val onShareClicked: (String) -> Unit, ) } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt deleted file mode 100644 index b1245ee718..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.feature.referral.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.google.accompanist.web.WebView -import com.google.accompanist.web.rememberWebViewState -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.webview.applySafeSettings -import com.tangem.feature.referral.presentation.R - -/** - * Bottom sheet content with referral program terms of use - * - * @param url link to the html page - * @param bottomBarHeight bottom insets - */ -@Composable -internal fun AgreementBottomSheetContent(url: String, bottomBarHeight: Dp) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - TangemTopAppBar(title = stringResource(id = R.string.details_referral_title)) - AgreementHtmlView(url = url) - Spacer(modifier = Modifier.height(bottomBarHeight)) - } -} - -@Composable -private fun AgreementHtmlView(url: String) { - val state = rememberWebViewState(url) - val isInPreviewMode = LocalInspectionMode.current - WebView( - state = state, - modifier = Modifier.background(TangemTheme.colors.background.primary), - captureBackPresses = false, - onCreated = { - if (!isInPreviewMode) it.applySafeSettings() - }, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AgreementBottomSheet() { - TangemThemePreview { - AgreementBottomSheetContent(url = "https://tangem.com/en/", 50.dp) - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index e005f91c7b..2dc7e37ea1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -9,20 +9,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -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.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R @Composable internal fun AgreementText(@StringRes firstPartResId: Int, onClick: () -> Unit) { - val agreementText = annotatedAgreementString(firstPart = stringResource(firstPartResId)) + val agreementText = annotatedAgreementString(firstPart = stringResourceSafe(firstPartResId)) ClickableText( text = agreementText, modifier = Modifier @@ -46,10 +46,10 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString { append("$firstPart ") } withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append(stringResource(id = R.string.common_terms_and_conditions)) + append(stringResourceSafe(id = R.string.common_terms_and_conditions)) } withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" ${stringResource(id = R.string.referral_tos_suffix)}") + append(" ${stringResourceSafe(id = R.string.referral_tos_suffix)}") } } } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt index 077fb745d0..0849b5314f 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R @@ -23,7 +23,7 @@ internal fun NonParticipateBottomBlock(onAgreementClick: () -> Unit, onParticipa ) PrimaryButtonIconEnd( - text = stringResource(id = R.string.referral_button_participate), + text = stringResourceSafe(id = R.string.referral_button_participate), iconResId = R.drawable.ic_tangem_24, onClick = onParticipateClick, modifier = Modifier diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 300498f2e8..12ebf8162c 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -1,7 +1,5 @@ package com.tangem.feature.referral.ui -import android.content.Context -import android.content.Intent import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background @@ -18,15 +16,15 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.core.content.ContextCompat.startActivity +import com.tangem.core.res.getStringSafe import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.extensions.pluralStringResourceSafe +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward @@ -44,7 +42,7 @@ internal fun ParticipateBottomBlock( snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, onCopyClick: () -> Unit, - onShareClick: () -> Unit, + onShareClick: (String) -> Unit, ) { Column( modifier = Modifier @@ -98,10 +96,10 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected @Composable private fun Counter(purchasedWalletCount: Int, overallItemsLastIndex: Int) { RoundableCornersRow( - startText = stringResource(id = R.string.referral_friends_bought_title), + startText = stringResourceSafe(id = R.string.referral_friends_bought_title), startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, - endText = pluralStringResource( + endText = pluralStringResourceSafe( id = R.plurals.referral_wallets_purchased_count, count = purchasedWalletCount, purchasedWalletCount, @@ -122,14 +120,14 @@ private fun Awards(expectedAwards: ExpectedAwards, overallItemsLastIndex: Int, i ) RoundableCornersRow( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { - stringResource(id = R.string.referral_expected_awards) + stringResourceSafe(id = R.string.referral_expected_awards) } else { - stringResource(id = R.string.referral_no_expected_awards) + stringResourceSafe(id = R.string.referral_no_expected_awards) }, startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, endText = if (expectedAwards.expectedAwards.isNotEmpty()) { - pluralStringResource( + pluralStringResourceSafe( id = R.plurals.referral_number_of_wallets, count = expectedAwards.numberOfWallets, expectedAwards.numberOfWallets, @@ -201,9 +199,9 @@ private fun LessMoreButton(isExpanded: MutableState) { ) { Text( text = if (isExpanded.value) { - stringResource(id = R.string.referral_less) + stringResourceSafe(id = R.string.referral_less) } else { - stringResource(id = R.string.referral_more) + stringResourceSafe(id = R.string.referral_more) }, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, @@ -261,7 +259,7 @@ private fun PersonalCodeCard(code: String) { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = stringResource(id = R.string.referral_promo_code_title), + text = stringResourceSafe(id = R.string.referral_promo_code_title), color = TangemTheme.colors.text.tertiary, maxLines = 1, style = TangemTheme.typography.subtitle2, @@ -281,7 +279,7 @@ private fun AdditionalButtons( shareLink: String, snackbarHostState: SnackbarHostState, onCopyClick: () -> Unit, - onShareClick: () -> Unit, + onShareClick: (String) -> Unit, ) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current @@ -294,7 +292,7 @@ private fun AdditionalButtons( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { PrimaryButtonIconStart( - text = stringResource(id = R.string.common_copy), + text = stringResourceSafe(id = R.string.common_copy), iconResId = R.drawable.ic_copy_24, onClick = { onCopyClick.invoke() @@ -303,7 +301,7 @@ private fun AdditionalButtons( coroutineScope.launch { snackbarHostState.showSnackbar( - message = resources.getString(R.string.referral_promo_code_copied), + message = resources.getStringSafe(R.string.referral_promo_code_copied), duration = SnackbarDuration.Short, ) } @@ -313,12 +311,11 @@ private fun AdditionalButtons( val context = LocalContext.current PrimaryButtonIconStart( - text = stringResource(id = R.string.common_share), + text = stringResourceSafe(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { - onShareClick.invoke() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(context.getString(R.string.referral_share_link, shareLink)) + onShareClick(context.getString(R.string.referral_share_link, shareLink)) }, modifier = Modifier.weight(1f), ) @@ -341,16 +338,6 @@ private fun calculateOverallItemsCount(expectedAwards: ExpectedAwards?, isExpand return counterItems + awardItems } -private fun Context.shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(this, shareIntent, null) -} - @Preview(widthDp = 360, showBackground = true) @Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -434,5 +421,5 @@ private data class ParticipateBottomBlockData( val onAgreementClick: () -> Unit = {}, val onShowCopySnackbar: () -> Unit = {}, val onCopyClick: () -> Unit = {}, - val onShareClick: () -> Unit = {}, + val onShareClick: (String) -> Unit = {}, ) \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt deleted file mode 100644 index 0a3d43118a..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.feature.referral.models.ReferralStateHolder - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun ReferralBottomSheet( - sheetState: SheetState, - isVisible: Boolean, - onDismissRequest: () -> Unit, - config: ReferralStateHolder.ReferralInfoState, -) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - if (isVisible) { - ModalBottomSheet( - modifier = Modifier.statusBarsPadding(), - onDismissRequest = onDismissRequest, - sheetState = sheetState, - shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ), - contentWindowInsets = { WindowInsetsZero }, - containerColor = TangemTheme.colors.background.primary, - ) { - AgreementBottomSheetContent( - url = when (config) { - is ReferralStateHolder.ReferralInfoState.NonParticipantContent -> config.url - is ReferralStateHolder.ReferralInfoState.ParticipantContent -> config.url - is ReferralStateHolder.ReferralInfoState.Loading -> "" - }, - bottomBarHeight = bottomBarHeight, - ) - } - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index a3a69e7bd8..f5941be08d 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -10,18 +10,21 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.res.getStringSafe import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 @@ -29,6 +32,7 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward @@ -43,14 +47,9 @@ import kotlinx.coroutines.launch * Referral program screen for participant and non-participant * * @param stateHolder state holder - * @param modifier modifier */ -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ReferralScreen(stateHolder: ReferralStateHolder) { - var isBottomSheetVisible by remember { mutableStateOf(value = false) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val snackbarHostState = remember(::SnackbarHostState) BackHandler(onBack = stateHolder.headerState.onBackClicked) @@ -59,7 +58,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) { topBar = { AppBarWithBackButton( modifier = Modifier.statusBarsPadding(), - text = stringResource(R.string.details_referral_title), + text = stringResourceSafe(R.string.details_referral_title), onBackClick = stateHolder.headerState.onBackClicked, ) }, @@ -83,10 +82,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) { ReferralContent( stateHolder = stateHolder, snackbarHostState = snackbarHostState, - onAgreementClick = { - stateHolder.analytics.onAgreementClicked.invoke() - isBottomSheetVisible = true - }, + onAgreementClick = stateHolder.analytics.onAgreementClicked, modifier = Modifier.padding(it), ) } @@ -100,7 +96,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) { coroutineScope.launch { val result = snackbarHostState.showSnackbar( message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable), - actionLabel = resources.getString(R.string.warning_button_ok), + actionLabel = resources.getStringSafe(R.string.warning_button_ok), duration = SnackbarDuration.Indefinite, ) @@ -110,13 +106,6 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) { } } } - - ReferralBottomSheet( - sheetState = sheetState, - isVisible = isBottomSheetVisible, - onDismissRequest = { isBottomSheetVisible = false }, - config = stateHolder.referralInfoState, - ) } @Composable @@ -156,7 +145,7 @@ private fun Header() { ) SpacerH24() Text( - text = stringResource(id = R.string.referral_title), + text = stringResourceSafe(id = R.string.referral_title), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing50), color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -273,7 +262,7 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> @Composable private fun InfoForYou(award: String, networkName: String, address: String? = null) { - ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { + ConditionInfo(title = stringResourceSafe(id = R.string.referral_point_currencies_title)) { Text( formatAwardConditionsString( quantity = award, @@ -288,7 +277,7 @@ private fun InfoForYou(award: String, networkName: String, address: String? = nu @Composable private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString { - val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address) + val rawString = stringResourceSafe(R.string.referral_point_currencies_description, quantity, network, address) val pattern = Regex("\\^\\^(.*?)\\^\\^") var startIndex = 0 @@ -318,20 +307,20 @@ private fun formatAwardConditionsString(quantity: String, network: String, addre @Composable private fun InfoForYourFriend(discount: String) { - ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { + ConditionInfo(title = stringResourceSafe(id = R.string.referral_point_discount_title)) { Text( text = buildAnnotatedString { - append(stringResource(id = R.string.referral_point_discount_description_prefix)) + append(stringResourceSafe(id = R.string.referral_point_discount_description_prefix)) withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { append( String.format( - stringResource(id = R.string.referral_point_discount_description_value), + stringResourceSafe(id = R.string.referral_point_discount_description_value), " $discount", ), ) } append(" ") - append(stringResource(id = R.string.referral_point_discount_description_suffix)) + append(stringResourceSafe(id = R.string.referral_point_discount_description_suffix)) }, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, @@ -374,9 +363,11 @@ private fun ShimmerInfo() { private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String { return when { - throwable is DemoModeException -> getString(R.string.alert_demo_feature_disabled) - throwable.cause != null -> getString(R.string.referral_error_failed_to_load_info_with_reason, throwable.cause) - else -> getString(R.string.referral_error_failed_to_load_info) + throwable is DemoModeException -> getStringSafe(R.string.alert_demo_feature_disabled) + throwable.cause != null -> { + getStringSafe(R.string.referral_error_failed_to_load_info_with_reason, requireNotNull(throwable.cause)) + } + else -> getStringSafe(R.string.referral_error_failed_to_load_info) } } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index 236d0c93db..a0a455ee28 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -10,6 +10,8 @@ import androidx.lifecycle.viewModelScope import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.referral.analytics.ReferralEvents import com.tangem.feature.referral.domain.ReferralInteractor @@ -34,6 +36,8 @@ internal class ReferralViewModel @Inject constructor( private val referralInteractor: ReferralInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, + private val shareManager: ShareManager, + private val urlOpener: UrlOpener, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -119,14 +123,18 @@ internal class ReferralViewModel @Inject constructor( private fun onAgreementClicked() { analyticsEventHandler.send(ReferralEvents.ClickTaC) + + lastReferralData?.tosLink?.let(urlOpener::openUrl) } private fun onCopyClicked() { analyticsEventHandler.send(ReferralEvents.ClickCopy) } - private fun onShareClicked() { + private fun onShareClicked(text: String) { analyticsEventHandler.send(ReferralEvents.ClickShare) + + shareManager.shareText(text = text) } private fun ReferralData.convertToReferralInfoState(): ReferralInfoState = when (this) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 5716d8be03..3918f10bb4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.ui.amountScreen.models.AmountState @@ -235,7 +236,7 @@ internal class SendNotificationFactory( addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, feeAmount = feeState.fee?.amount?.value.orZero(), - receivedAmount = sendingAmount, + sendingAmount = sendingAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, onReduceClick = clickIntents::onAmountReduceByClick, ) @@ -248,7 +249,7 @@ internal class SendNotificationFactory( ) addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState.fee, + minAdaValue = (feeState.fee as? Fee.CardanoToken)?.minAdaValue, validationError = validationError, cryptoCurrency = currency, onReduceClick = clickIntents::onAmountReduceToClick, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 7e17a6e883..ca5adcd00a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -59,13 +59,14 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onExploreClick() {} - override fun onShareClick() {} + override fun onShareClick(txUrl: String) {} override fun onAmountReduceByClick( reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal, notification: Class, - ) {} + ) { + } override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) {} diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 27229a8cd5..aa5604d797 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -4,13 +4,13 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendEvent @@ -58,7 +58,7 @@ internal fun SendAlert(state: AlertUM, onDismiss: () -> Unit) { }, ) dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 4c59863b0c..f1a3d33ffc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState @@ -69,7 +68,7 @@ internal fun SendNavigationButtons( SendDoneButtons( txUrl = sendState.txUrl, onExploreClick = uiState.clickIntents::onExploreClick, - onShareClick = uiState.clickIntents::onShareClick, + onShareClick = { uiState.clickIntents.onShareClick(it) }, isVisible = isSentState, ) SendNavigationButton( @@ -131,7 +130,7 @@ private fun SendNavigationButton( } TangemButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(buttonTextId), + text = stringResourceSafe(buttonTextId), icon = buttonIcon, enabled = isButtonEnabled, onClick = { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 096fbe1d8b..2c83d49d07 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -18,6 +17,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R @@ -76,8 +76,8 @@ internal fun SendSpeedSelector( @Composable private fun FooterText(onReadMoreClick: () -> Unit) { - val linkText = stringResource(R.string.common_read_more) - val fullString = stringResource(R.string.common_fee_selector_footer, linkText) + val linkText = stringResourceSafe(R.string.common_read_more) + val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText) val linkTextPosition = fullString.length - linkText.length val defaultStyle = TangemTheme.colors.text.tertiary val linkStyle = TangemTheme.colors.text.accent diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index ea78244e14..aaa589bde5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -17,16 +17,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowRecipient -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R @@ -167,7 +163,7 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo TangemTheme.dimens.radius0 } Text( - text = stringResource(titleRes), + text = stringResourceSafe(titleRes), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 4d5830190e..99c44da3df 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -10,7 +10,6 @@ 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.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -18,6 +17,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format @@ -41,7 +41,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResource(R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.secondary, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index d65fdea64d..ff3fda113f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -20,13 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -122,11 +122,12 @@ private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modif ).plus(fadeIn()), exit = slideOutVertically().plus(fadeOut()), ) { + modifier + .fillMaxWidth() Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .animateItemPlacement() + modifier = Modifier + .animateItem(fadeInSpec = null, fadeOutSpec = null) .padding(top = TangemTheme.dimens.spacing20), ) { val background = TangemTheme.colors.button.secondary @@ -137,7 +138,7 @@ private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modif modifier = Modifier, ) Text( - text = stringResource(id = R.string.send_summary_tap_hint), + text = stringResourceSafe(id = R.string.send_summary_tap_hint), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 57247c99ba..e77536f76c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -55,7 +55,7 @@ internal interface SendClickIntents : AmountScreenClickIntents { fun onExploreClick() - fun onShareClick() + fun onShareClick(txUrl: String) fun onAmountReduceByClick( reduceAmountBy: BigDecimal, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 8b0778f2cd..ec9e878040 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -15,6 +15,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -106,6 +107,7 @@ internal class SendViewModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, validateTransactionUseCase: ValidateTransactionUseCase, getCurrencyCheckUseCase: GetCurrencyCheckUseCase, @@ -862,8 +864,9 @@ internal class SendViewModel @Inject constructor( innerRouter.openUrl(sendState.txUrl) } - override fun onShareClick() { + override fun onShareClick(txUrl: String) { analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + shareManager.shareText(txUrl) } override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt index a1b9cce6d9..7e7700595e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt @@ -10,6 +10,4 @@ internal sealed class StakingEvent { data class ShowSnackBar(val text: TextReference) : StakingEvent() data class ShowAlert(val alert: AlertUM) : StakingEvent() - - data class ShowShareDialog(val txUrl: String) : StakingEvent() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 85c5470976..75bcbd3efd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -41,9 +41,4 @@ internal class StakingEventFactory( ) stateController.updateEvent(alert) } - - fun createShareDialog(txUrl: String) { - val event = StakingEvent.ShowShareDialog(txUrl) - stateController.updateEvent(event) - } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt index 2c927652c7..c70861712b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt @@ -17,7 +17,7 @@ internal class ShowActionSelectorBottomSheetTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, content = StakingActionSelectionBottomSheetConfig( title = resourceReference(R.string.common_choose_action), actions = pendingActions, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index f0e0ade2cc..9f1c669b3e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -17,7 +17,7 @@ internal class ShowInfoBottomSheetStateTransformer( return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( onDismissRequest = onDismiss, - isShow = true, + isShown = true, content = when (infoType) { InfoType.ANNUAL_PERCENTAGE_RATE -> StakingInfoBottomSheetConfig( title = resourceReference(R.string.staking_details_annual_percentage_rate), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt index 085061c2b8..5ceb9ac4d3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt @@ -13,7 +13,7 @@ internal class SetApprovalBottomSheetInProgressTransformer( return prevState.copy( bottomSheetConfig = prevState.bottomSheetConfig?.copy( onDismissRequest = onDismiss, - isShow = true, + isShown = true, content = approvalBottomSheetConfig?.let { config -> config.copy( data = config.data.copy( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index 8350e74f74..be9aac4f36 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -45,7 +45,7 @@ internal class ShowApprovalBottomSheetTransformer( ) return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = GiveTxPermissionBottomSheetConfig( data = GiveTxPermissionState.ReadyForRequest( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index d96cda409d..c46880d541 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.notifications +import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification @@ -197,7 +198,7 @@ internal class AddStakingNotificationsTransformer( addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, feeAmount = feeState?.fee?.amount?.value.orZero(), - receivedAmount = sendingAmount, + sendingAmount = sendingAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, onReduceClick = prevState.clickIntents::onAmountReduceByClick, ) @@ -212,7 +213,7 @@ internal class AddStakingNotificationsTransformer( // blockchain specific addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState?.fee, + minAdaValue = (feeState?.fee as? Fee.CardanoToken)?.minAdaValue, validationError = validatorError, cryptoCurrency = cryptoCurrency, onReduceClick = prevState.clickIntents::onAmountReduceToClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt index 18426cab00..96234a843e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -4,20 +4,18 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.events.StakingEvent @Composable internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { - val context = LocalContext.current val resources = LocalContext.current.resources var alertConfig by remember { mutableStateOf(value = null) } @@ -40,9 +38,6 @@ internal fun StakingEventEffect(event: StateEvent, snackbarHostSta is StakingEvent.ShowAlert -> { alertConfig = value.alert } - is StakingEvent.ShowShareDialog -> { - context.shareText(value.txUrl) - } } }, ) @@ -64,7 +59,7 @@ internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { ) dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 95038b16d9..d213a829f3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.Brush 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.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -144,7 +143,7 @@ private fun LazyListScope.activeStakingBlock( if (innerYieldBalanceState.balances.isNotEmpty()) { item(ACTIVE_STAKING_BLOCK_KEY) { Text( - text = stringResource(id = R.string.staking_your_stakes), + text = stringResourceSafe(id = R.string.staking_your_stakes), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier @@ -214,7 +213,7 @@ private fun BannerBlock(onClick: () -> Unit) { .padding(TangemTheme.dimens.spacing16), text = buildAnnotatedString { withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { - append(stringResource(R.string.staking_details_banner_text)) + append(stringResourceSafe(R.string.staking_details_banner_text)) } }, style = TangemTheme.typography.h2, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt index 2f239888e8..b5a6372037 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -2,10 +2,10 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.appendColored +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R @@ -17,9 +17,9 @@ private const val PRIVACY_POLICY_URL = "https://docs.stakek.it/docs/privacy-poli @Composable internal fun StakingTosText(onTextClick: (String) -> Unit) { - val termsOfUse = stringResource(R.string.common_terms_of_use) - val privacyPolicy = stringResource(R.string.common_privacy_policy) - val tosText = stringResource(R.string.staking_legal, termsOfUse, privacyPolicy) + val termsOfUse = stringResourceSafe(R.string.common_terms_of_use) + val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy) + val tosText = stringResourceSafe(R.string.staking_legal, termsOfUse, privacyPolicy) val clickableAnnotation = buildAnnotatedString { append(tosText.substringBefore(termsOfUse)) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 1bcd5c9c60..6afc60b7b3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -113,7 +112,7 @@ internal fun StakingValidatorListContent( private fun RowScope.ValidatorLabel(isStrategicPartner: Boolean) { if (isStrategicPartner) { Text( - text = stringResource(R.string.staking_validators_label), + text = stringResourceSafe(R.string.staking_validators_label), style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.constant, modifier = Modifier diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 8817faede7..cfd283c0d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -9,7 +9,6 @@ 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.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -21,6 +20,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format @@ -46,7 +46,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResource(R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt index 8f4434e496..64c371054c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -67,7 +67,7 @@ private fun Preview_StakingActionSelectorBottomSheet() { TangemThemePreview { StakingActionSelectorBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = StakingActionSelectionBottomSheetConfig( title = resourceReference(R.string.common_select_action), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt index 6e0b80bd18..af0ceabcc0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt @@ -52,7 +52,7 @@ private fun Preview_StakingInfoBottomSheet() { TangemThemePreview { StakingInfoBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = StakingInfoBottomSheetConfig( title = stringReference("Title"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index adadecbac9..58f5125717 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -17,6 +17,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomS import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -119,6 +120,7 @@ internal class StakingViewModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getActionsUseCase: GetActionsUseCase, private val paramsInterceptorHolder: ParamsInterceptorHolder, + private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -765,7 +767,7 @@ internal class StakingViewModel @Inject constructor( analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare) if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - stakingEventFactory.createShareDialog(txUrl = txUrl) + shareManager.shareText(txUrl) } } diff --git a/features/swap/api/build.gradle.kts b/features/swap/api/build.gradle.kts index 76bb25817f..099edf86ca 100644 --- a/features/swap/api/build.gradle.kts +++ b/features/swap/api/build.gradle.kts @@ -1,18 +1,10 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.kotlin.serialization) - alias(deps.plugins.hilt.android) + id("kotlin-parcelize") id("configuration") } android { - namespace = "com.tangem.feature.swap.api" -} - -dependencies { - /** DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) + namespace = "com.tangem.features.swap.api" } \ No newline at end of file diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt new file mode 100644 index 0000000000..f43c34f8ac --- /dev/null +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.swap + +interface SwapFeatureToggles { + val isPromoStoriesEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt deleted file mode 100644 index 4d0d6904c4..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -import java.math.BigDecimal - -sealed class Warning { - - data class ExistentialDepositWarning( - val existentialDeposit: BigDecimal, - val minAvailableAmount: BigDecimal, - ) : Warning() - - data class MinAmountWarning(val dustValue: BigDecimal) : Warning() - - data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() - - sealed class Cardano : Warning() { - - data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano() - - data object InsufficientBalanceToTransferCoin : Cardano() - - data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano() - } -} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 6183efe111..98c167b26c 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* @@ -26,7 +27,9 @@ sealed interface SwapState { val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, val txFee: TxFeeState, - val warnings: List = emptyList(), + val currencyCheck: CryptoCurrencyCheck? = null, + val validationResult: Throwable? = null, + val minAdaValue: BigDecimal?, val swapProvider: SwapProvider, ) : SwapState @@ -93,7 +96,15 @@ sealed class TxFeeState { data class MultipleFeeState( val normalFee: TxFee, val priorityFee: TxFee, - ) : TxFeeState() + ) : TxFeeState() { + + fun getFeeByType(feeType: FeeType): TxFee { + return when (feeType) { + FeeType.NORMAL -> normalFee + FeeType.PRIORITY -> priorityFee + } + } + } data class SingleFeeState( val fee: TxFee, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 3154fa4293..0eef2da3aa 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -9,6 +9,7 @@ import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.* +import java.math.BigDecimal interface SwapInteractor { @@ -39,6 +40,7 @@ interface SwapInteractor { toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, + reduceBalanceBy: BigDecimal, selectedFee: FeeType = FeeType.NORMAL, ): Map @@ -65,12 +67,13 @@ interface SwapInteractor { fee: TxFee, ): SwapTransactionState - suspend fun updateQuotesStateWithSelectedFee( - state: SwapState.QuotesLoadedState, - selectedFee: FeeType, - fromToken: CryptoCurrencyStatus, - amountToSwap: String, - ): SwapState.QuotesLoadedState + // suspend fun updateQuotesStateWithSelectedFee( + // state: SwapState.QuotesLoadedState, + // selectedFee: FeeType, + // fromToken: CryptoCurrencyStatus, + // amountToSwap: String, + // reduceBalanceBy: BigDecimal, + // ): SwapState.QuotesLoadedState /** * Returns token in wallet balance diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 6ad0d0ff86..ad0e992a73 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,23 +2,23 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse -import com.tangem.blockchain.common.* import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain.* +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase +import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -36,7 +36,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount @@ -71,6 +70,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val amountFormatter: AmountFormatter, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -251,6 +251,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, + reduceBalanceBy: BigDecimal, selectedFee: FeeType, ): Map { Timber.i( @@ -291,7 +292,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken = toToken, provider = provider, amount = amount, + reduceBalanceBy = reduceBalanceBy, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + selectedFee = selectedFee, ) } } @@ -359,6 +362,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFee = TxFeeState.Empty, transactionFee = null, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + selectedFee = selectedFee, ) } } @@ -369,16 +373,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken: CryptoCurrencyStatus, provider: SwapProvider, amount: SwapAmount, + reduceBalanceBy: BigDecimal, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): Pair { return provider to loadCexQuoteData( networkId = networkId, amount = amount, + reduceBalanceBy = reduceBalanceBy, fromTokenStatus = fromToken, toTokenStatus = toToken, isAllowedToSpend = true, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, + selectedFee = selectedFee, ) } @@ -386,111 +394,71 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, - minAdaValue: BigDecimal?, - ): List { - val fromToken = fromTokenStatus.currency - val warnings = mutableListOf() - manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) - manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) - manageReduceAmountWarning(warnings, fromTokenStatus, amount) - manageTransactionValidationWarnings( - warnings = warnings, - fromToken = fromToken, - amount = amount, - feeState = feeState, - userWalletId = userWalletId, - minAdaValue = minAdaValue, - ) - return warnings - } - - private suspend fun manageExistentialDepositWarning( - warnings: MutableList, - userWalletId: UserWalletId, - amount: SwapAmount, - fromToken: CryptoCurrency, - txFee: TxFeeState, - ) { - val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, fromToken.network) - if (existentialDeposit != null) { - val nativeBalance = userWalletManager.getNativeTokenBalance( - fromToken.network.backendId, - fromToken.network.derivationPath.value, - ) ?: ProxyAmount.empty() - // ignore if amount is bigger than balance - if (amount.value > nativeBalance.value) { - return - } - val fee = when (txFee) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue - } - val minAvailableAmount = nativeBalance.value - existentialDeposit - fee - if (nativeBalance.value.minus(amount.value + fee) < existentialDeposit) { - warnings.add(Warning.ExistentialDepositWarning(existentialDeposit, minAvailableAmount)) - } - } - } - - private suspend fun manageDustWarning( - warnings: MutableList, - feeState: TxFeeState, - userWalletId: UserWalletId, - fromTokenStatus: CryptoCurrencyStatus, - amount: SwapAmount, - ) { - if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return - + selectedFee: FeeType, + includeFeeInAmount: IncludeFeeInAmount, + ): CryptoCurrencyCheck { val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue + is TxFeeState.MultipleFeeState -> feeState.getFeeByType(selectedFee).feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } + val balanceAfterTransaction = getCoinBalanceAfterTransaction(fromTokenStatus, amount, includeFeeInAmount, fee) - val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return + val currencyCheck = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = fromTokenStatus, + amount = amount.value, + fee = fee, + balanceAfterTransaction = balanceAfterTransaction, + ) - val change = when (fromTokenStatus.currency) { - is CryptoCurrency.Coin -> { - val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO - balance - (fee + amount.value) - } - is CryptoCurrency.Token -> { - val nativeTokenBalance = userWalletManager.getNativeTokenBalance( - fromTokenStatus.currency.network.id.value, - fromTokenStatus.currency.network.derivationPath.value, - ) - - nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO - } - } - - val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO - - if (amount.value < dustValue || isChangeLowerThanDust) { - warnings.add(Warning.MinAmountWarning(dustValue)) - } + return currencyCheck } - private fun manageReduceAmountWarning( - warnings: MutableList, + private suspend fun getCoinBalanceAfterTransaction( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, - ) { - val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id - if (isTezos && amount.value == fromTokenStatus.value.amount) { - warnings.add(Warning.ReduceAmountWarning(Blockchain.Tezos.minimalAmount())) + includeFeeInAmount: IncludeFeeInAmount, + fee: BigDecimal, + ): BigDecimal? { + return when (fromTokenStatus.currency) { + is CryptoCurrency.Coin -> { + val statusValue = fromTokenStatus.value as? CryptoCurrencyStatus.Loaded + when (includeFeeInAmount) { + is IncludeFeeInAmount.Included -> { + statusValue?.let { it.amount - includeFeeInAmount.amountSubtractFee.value - fee } + } + is IncludeFeeInAmount.Excluded -> { + statusValue?.let { it.amount - amount.value - fee } + } + else -> null + } + } + is CryptoCurrency.Token -> { + val feePaidCurrency = getFeePaidCurrency( + currency = fromTokenStatus.currency, + ) + when (feePaidCurrency) { + FeePaidCurrency.Coin -> { + val nativeBalance = userWalletManager.getNativeTokenBalance( + networkId = fromTokenStatus.currency.network.backendId, + derivationPath = fromTokenStatus.currency.network.derivationPath.value, + ) + nativeBalance?.let { it.value - fee } + } + else -> null // it doesnt matter for this fun + } + } } } private suspend fun manageTransactionValidationWarnings( - warnings: MutableList, - fromToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, userWalletId: UserWalletId, - minAdaValue: BigDecimal?, - ) { + ): Throwable? { + val currency = fromToken.currency val fee = Fee.Common( amount = Amount( value = when (feeState) { @@ -498,66 +466,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue }, - blockchain = Blockchain.fromId(fromToken.network.id.value), + blockchain = Blockchain.fromId(currency.network.id.value), ), ) - validateTransactionUseCase( - amount = amount.value.convertToSdkAmount(fromToken), + val result = validateTransactionUseCase( + amount = amount.value.convertToSdkAmount(currency), fee = fee, memo = null, - destination = getTokenAddress(fromToken), + destination = getTokenAddress(fromToken.currency), userWalletId = userWalletId, - network = fromToken.network, - ).fold( - ifLeft = { - addCardanoTransactionValidationError( - warnings = warnings, - error = it as? BlockchainSdkError.Cardano ?: return@fold, - fromToken = fromToken, - userWalletId = userWalletId, - ) - }, - ifRight = { - minAdaValue?.let { - warnings.add( - Warning.Cardano.MinAdaValueCharged( - tokenName = fromToken.name, - minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), - ), - ) - } - }, - ) - } + network = currency.network, + ).leftOrNull() - private suspend fun addCardanoTransactionValidationError( - warnings: MutableList, - error: BlockchainSdkError.Cardano, - fromToken: CryptoCurrency, - userWalletId: UserWalletId, - ) { - when (error) { - BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { - Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) - } - BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { - when (fromToken) { - is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin - is CryptoCurrency.Token -> { - Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) - } - } - } - BlockchainSdkError.Cardano.InsufficientRemainingBalance, - BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, - -> { - val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return - - Warning.MinAmountWarning(dustValue) - } - } - .let(warnings::add) // add warning to the list + return result } override suspend fun onSwap( @@ -616,43 +538,46 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - override suspend fun updateQuotesStateWithSelectedFee( - state: SwapState.QuotesLoadedState, - selectedFee: FeeType, - fromToken: CryptoCurrencyStatus, - amountToSwap: String, - ): SwapState.QuotesLoadedState { - val amountDecimal = toBigDecimalOrNull(amountToSwap) - if (amountDecimal == null || amountDecimal.signum() == 0) { - return state - } - val amount = SwapAmount(amountDecimal, fromToken.currency.decimals) - val includeFeeInAmount = getIncludeFeeInAmount( - networkId = fromToken.currency.network.backendId, - txFee = state.txFee, - amount = amount, - fromToken = fromToken.currency, - ) - val fee = when (val txFee = state.txFee) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee - is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee - } - val feeState = getFeeState( - fee = fee, - spendAmount = amount, - networkId = fromToken.currency.network.backendId, - fromTokenStatus = fromToken, - ) - return state.copy( - permissionState = PermissionDataState.Empty, - preparedSwapConfigState = state.preparedSwapConfigState.copy( - feeState = feeState, - isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, - includeFeeInAmount = includeFeeInAmount, - ), - ) - } + // override suspend fun updateQuotesStateWithSelectedFee( + // state: SwapState.QuotesLoadedState, + // selectedFee: FeeType, + // fromToken: CryptoCurrencyStatus, + // amountToSwap: String, + // reduceBalanceBy: BigDecimal, + // ): SwapState.QuotesLoadedState { + // val amountDecimal = toBigDecimalOrNull(amountToSwap) + // if (amountDecimal == null || amountDecimal.signum() == 0) { + // return state + // } + // val amount = SwapAmount(amountDecimal, fromToken.currency.decimals) + // val includeFeeInAmount = getIncludeFeeInAmount( + // networkId = fromToken.currency.network.backendId, + // txFee = state.txFee, + // amount = amount, + // reduceBalanceBy = reduceBalanceBy, + // fromToken = fromToken.currency, + // selectedFee = selectedFee, + // ) + // val fee = when (val txFee = state.txFee) { + // TxFeeState.Empty -> BigDecimal.ZERO + // is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + // is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee + // } + // val feeState = getFeeState( + // fee = fee, + // spendAmount = amount, + // networkId = fromToken.currency.network.backendId, + // fromTokenStatus = fromToken, + // ) + // return state.copy( + // permissionState = PermissionDataState.Empty, + // preparedSwapConfigState = state.preparedSwapConfigState.copy( + // feeState = feeState, + // isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, + // includeFeeInAmount = includeFeeInAmount, + // ), + // ) + // } private suspend fun onSwapDex( provider: SwapProvider, @@ -1018,6 +943,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( PolygonZkEVM, PolygonZkEVMTestnet, Radiant, + Fact0rn, Base, BaseTestnet, Moonriver, @@ -1050,6 +976,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( Chiliz, ChilizTestnet, Clore, + VanarChain, + VanarChainTestnet, + OdysseyChain, OdysseyChainTestnet, + Bitrock, BitrockTestnet, -> Fee.Common(feeAmount) // endregion } @@ -1150,11 +1080,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun loadCexQuoteData( networkId: String, amount: SwapAmount, + reduceBalanceBy: BigDecimal, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus, provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -1175,7 +1107,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId = networkId, txFee = txFee, amount = amount, + reduceBalanceBy = reduceBalanceBy, fromToken = fromToken, + selectedFee = selectedFee, ) val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { includeFeeInAmount.amountSubtractFee @@ -1206,8 +1140,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = txFee, - transactionFee = txFeeResult?.getOrNull(), + transactionFee = txFeeResult.getOrNull(), includeFeeInAmount = includeFeeInAmount, + selectedFee = selectedFee, ) } } @@ -1225,6 +1160,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFee: TxFeeState, transactionFee: TransactionFee?, includeFeeInAmount: IncludeFeeInAmount, + selectedFee: FeeType, ): SwapState { return quoteDataModel.fold( ifRight = { quoteModel -> @@ -1238,12 +1174,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFeeState = txFee, provider = provider, ).copy( - warnings = manageWarnings( + currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, feeState = txFee, - minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, + selectedFee = selectedFee, + includeFeeInAmount = includeFeeInAmount, ), + validationResult = manageTransactionValidationWarnings( + fromToken = fromToken, + amount = amount, + feeState = txFee, + userWalletId = userWalletId, + ), + minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, ) when (provider.type) { @@ -1321,11 +1265,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId: String, txFee: TxFeeState, amount: SwapAmount, + reduceBalanceBy: BigDecimal, fromToken: CryptoCurrency, + selectedFee: FeeType, ): IncludeFeeInAmount { val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + is TxFeeState.MultipleFeeState -> txFee.getFeeByType(selectedFee).feeIncludeOtherNativeFee is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feePaidCurrency = getFeePaidCurrency( @@ -1340,13 +1286,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( IncludeFeeInAmount.BalanceNotEnough } } - else -> getIncludeFeeAmountForCoinFee(networkId, amount, feeValue, fromToken) + else -> getIncludeFeeAmountForCoinFee( + networkId = networkId, + amount = amount, + reduceBalanceBy = reduceBalanceBy, + feeValue = feeValue, + fromToken = fromToken, + ) } } private suspend fun getIncludeFeeAmountForCoinFee( networkId: String, amount: SwapAmount, + reduceBalanceBy: BigDecimal, feeValue: BigDecimal, fromToken: CryptoCurrency, ): IncludeFeeInAmount { @@ -1355,26 +1308,27 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId, fromToken.network.derivationPath.value, ) ?: ProxyAmount.empty() + val reducedBalance = tokenForFeeBalance.value - reduceBalanceBy val amountWithFee = amount.value + feeValue return when { fromToken is CryptoCurrency.Token -> { - if (feeValue > tokenForFeeBalance.value || tokenForFeeBalance.value.signum() == 0) { + if (feeValue > reducedBalance || reducedBalance.signum() == 0) { IncludeFeeInAmount.BalanceNotEnough } else { IncludeFeeInAmount.Excluded } } - amount.value > tokenForFeeBalance.value -> { + amount.value > reducedBalance -> { IncludeFeeInAmount.BalanceNotEnough } - amountWithFee < tokenForFeeBalance.value -> { + amountWithFee < reducedBalance -> { IncludeFeeInAmount.Excluded } else -> { if (feeValue < amount.value) { IncludeFeeInAmount.Included( SwapAmount( - tokenForFeeBalance.value - feeValue, + reducedBalance - feeValue, getNativeToken(fromToken.network.backendId).decimals, ), ) @@ -1450,6 +1404,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) } + val includeFeeInAmount = IncludeFeeInAmount.Excluded // exclude for dex val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO) val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeToCheckFunds) @@ -1464,7 +1419,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( isBalanceEnough = isBalanceIncludeFeeEnough, feeState = feeState, hasOutgoingTransaction = hasOutgoingTransaction(fromToken), - includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + includeFeeInAmount = includeFeeInAmount, ) val swapState = updateBalances( networkId = networkId, @@ -1478,11 +1433,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings( + currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, feeState = txFeeState, - minAdaValue = null, // no ADA in DEX + selectedFee = selectedFee, + includeFeeInAmount = includeFeeInAmount, + ), + validationResult = manageTransactionValidationWarnings( + fromToken = fromToken, + amount = amount, + feeState = txFeeState, + userWalletId = userWalletId, ), preparedSwapConfigState = preparedSwapConfigState, ) @@ -1578,6 +1540,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapDataModel = swapData, txFee = txFeeState, swapProvider = provider, + minAdaValue = null, ) } @@ -1600,20 +1563,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount, userWallet = userWallet, cryptoCurrency = cryptoCurrency, - ).mapFeeToForceSingleFeeIfNeeded(cryptoCurrency) - } - - private fun Either.mapFeeToForceSingleFeeIfNeeded( - cryptoCurrency: CryptoCurrency, - ): Either { - return this.map { - val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) - if (it is TransactionFee.Choosable && forceSingleFeeBlockchains.contains(blockchain)) { - TransactionFee.Single(normal = it.normal) - } else { - it - } - } + ) } @Suppress("LongParameterList", "LongMethod") @@ -2227,17 +2177,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val minDemoFee = "0.0001".toBigDecimal() private val normalDemoFee = "0.0002".toBigDecimal() private val priorityDemoFee = "0.0003".toBigDecimal() - - private val forceSingleFeeBlockchains = listOf( - Bitcoin, BitcoinTestnet, - BitcoinCash, BitcoinCashTestnet, - Litecoin, - Dogecoin, - Dash, - Kaspa, - Ravencoin, RavencoinTestnet, - Ducatus, - ) } @AssistedFactory diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index ddbb0c1e9a..3049182a5f 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -38,12 +38,14 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.staking) implementation(projects.domain.feedback) + implementation(projects.domain.promo) /** Feature modules */ implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) implementation(projects.features.wallet.api) + implementation(projects.features.swap.api) /** AndroidX */ implementation(deps.androidx.activity.compose) @@ -60,7 +62,6 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) - implementation(deps.compose.accompanist.systemUiController) /** Api */ implementation(projects.features.swap.api) @@ -71,7 +72,6 @@ dependencies { /** Other libraries */ implementation(deps.compose.shimmer) - implementation(deps.compose.accompanist.webView) implementation(deps.compose.accompanist.systemUiController) implementation(deps.kotlin.serialization) implementation(deps.kotlin.immutable.collections) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt new file mode 100644 index 0000000000..cd341751d8 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.swap + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.SwapFeatureToggles + +internal class DefaultSwapFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : SwapFeatureToggles { + override val isPromoStoriesEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SWAP_STORIES_ENABLED") +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt new file mode 100644 index 0000000000..f2b92be438 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.swap.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.feature.swap.DefaultSwapFeatureToggles +import com.tangem.features.swap.SwapFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SwapFeatureModule { + + @Provides + @Singleton + fun provideSwapFeatureToggles(featureToggles: FeatureTogglesManager): SwapFeatureToggles { + return DefaultSwapFeatureToggles(featureToggles) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 8b21ec6919..677d1d7e01 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -3,9 +3,9 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -14,12 +14,15 @@ import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.events.SwapEvent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf internal data class SwapStateHolder( val sendCardData: SwapCardState, val receiveCardData: SwapCardState, val blockchainId: String, // not the same as networkId, its local id in app - val warnings: List = emptyList(), + val notifications: ImmutableList = persistentListOf(), + val isInsufficientFunds: Boolean, val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, @@ -31,6 +34,7 @@ internal data class SwapStateHolder( val successState: SwapSuccessStateHolder? = null, val selectTokenState: SwapSelectTokenStateHolder? = null, val bottomSheetConfig: TangemBottomSheetConfig? = null, + val storiesConfig: SwapStoriesContentConfig? = null, val swapButton: SwapButton, val shouldShowMaxAmount: Boolean, @@ -39,8 +43,8 @@ internal data class SwapStateHolder( val onRefresh: () -> Unit, val onBackClicked: () -> Unit, val onChangeCardsClicked: () -> Unit, - val onSelectTokenClick: (() -> Unit)? = null, - val onSuccess: (() -> Unit)? = null, + val onSelectTokenClick: (() -> Unit), + val onSuccess: (() -> Unit), val onMaxAmountSelected: (() -> Unit)? = null, val onShowPermissionBottomSheet: () -> Unit = {}, ) @@ -78,21 +82,27 @@ data class SwapButton( sealed interface TransactionCardType { val header: TextReference - val isError: Boolean + val inputError: InputError data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), - override val isError: Boolean, + override val inputError: InputError, override val header: TextReference = TextReference.Res(R.string.swapping_from_title), ) : TransactionCardType data class ReadOnly( val showWarning: Boolean = false, val onWarningClick: (() -> Unit)? = null, - override val isError: Boolean = false, + override val inputError: InputError = InputError.Empty, override val header: TextReference = TextReference.Res(R.string.swapping_to_title), ) : TransactionCardType + + sealed interface InputError { + data object Empty : InputError + data object InsufficientFunds : InputError + data object WrongAmount : InputError + } } data class TosState( @@ -106,35 +116,6 @@ data class LegalState( val onClick: (String) -> Unit, ) -sealed interface SwapWarning { - data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning - data object InsufficientFunds : SwapWarning - data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning - data class GenericWarning( - val title: TextReference? = null, - val message: TextReference? = null, - val onClick: () -> Unit, - ) : SwapWarning - - data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning - data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning - data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning - data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning - data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning - data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning - data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning - - sealed interface Cardano : SwapWarning { - val notificationConfig: NotificationConfig - - data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano - - data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano - - data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano - } -} - enum class ChangeCardsButtonState { ENABLED, DISABLED, UPDATE_IN_PROGRESS } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStoriesContentConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStoriesContentConfig.kt new file mode 100644 index 0000000000..625eecdb62 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStoriesContentConfig.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.swap.models + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.stories.model.StoriesContentConfig +import com.tangem.core.ui.components.stories.model.StoryConfig +import kotlinx.collections.immutable.ImmutableList + +data class SwapStoriesContentConfig( + override val stories: ImmutableList, + override val onClose: () -> Unit, +) : StoriesContentConfig { + override val isRestartable: Boolean = false +} + +data class SwapStoryConfig( + @DrawableRes val imageRes: Int, +) : StoryConfig { + override val duration: Int = 2000 +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index c25fc38225..4c7a41a979 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -4,6 +4,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee +import java.math.BigDecimal data class UiActions( val onSearchEntered: (String) -> Unit, @@ -15,19 +16,20 @@ data class UiActions( val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, - val onReduceAmount: (SwapAmount) -> Unit, - val onLeaveExistentialDeposit: (SwapAmount) -> Unit, + val onReduceToAmount: (SwapAmount) -> Unit, + val onReduceByAmount: (SwapAmount, reduceBy: BigDecimal) -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions + val onStoriesClose: () -> Unit, val onRetryClick: () -> Unit, val onClickFee: () -> Unit, val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, val onBuyClick: (CryptoCurrency) -> Unit, - val onPolicyClick: (String) -> Unit, - val onTosClick: (String) -> Unit, + val onSelectTokenClick: () -> Unit, + val onSuccess: () -> Unit, + val onLinkClick: (String) -> Unit, val onReceiveCardWarningClick: () -> Unit, - val onFeeReadMoreClick: (String) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt new file mode 100644 index 0000000000..7736b9a69d --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -0,0 +1,205 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.common.ui.R +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.utils.getExpressErrorMessage +import com.tangem.feature.swap.utils.getExpressErrorTitle + +internal object SwapNotificationUM { + + sealed class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM.Error( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonState = buttonState, + onCloseClick = onCloseClick, + ) { + data class GenericError( + val title: TextReference = resourceReference(id = R.string.common_warning), + val subtitle: TextReference?, + val onConfirmClick: () -> Unit, + ) : Error( + title = title, + subtitle = subtitle ?: resourceReference(id = R.string.common_unknown_error), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.common_ok), + onClick = onConfirmClick, + ), + ) + + data object ApprovalInProgressWarning : Error( + title = resourceReference(R.string.warning_express_approval_in_progress_title), + subtitle = resourceReference(R.string.warning_express_approval_in_progress_message), + ) + + data class TransactionInProgressWarning( + val currencySymbol: String, + ) : Error( + title = resourceReference(R.string.warning_express_active_transaction_title), + subtitle = resourceReference( + id = R.string.warning_express_active_transaction_message, + formatArgs = wrappedList(currencySymbol), + ), + ) + + data class UnableToCoverFeeWarning( + val fromToken: CryptoCurrency, + val currencyName: String, + val currencySymbol: String, + val feeCurrency: CryptoCurrency?, + val onConfirmClick: (CryptoCurrency) -> Unit, + ) : Error( + title = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_title, + wrappedList(fromToken.network.name), + ), + subtitle = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_description, + wrappedList(currencyName, currencySymbol), + ), + iconResId = fromToken.networkIconResId, + buttonState = feeCurrency?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)), + onClick = { onConfirmClick(it) }, + ) + }, + ) + + data class MinimalAmountError( + val amount: String, + ) : Error( + title = resourceReference( + id = R.string.warning_express_too_minimal_amount_title, + formatArgs = wrappedList(amount), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + ) + + data class MaximumAmountError( + val amount: String, + ) : Error( + title = resourceReference( + id = R.string.warning_express_too_maximum_amount_title, + formatArgs = wrappedList(amount), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ) + } + + sealed class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM.Warning( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ) { + data class NoAvailableTokensToSwap( + val tokenName: String, + ) : Warning( + title = resourceReference( + com.tangem.feature.swap.presentation.R.string.warning_express_no_exchangeable_coins_title, + ), + subtitle = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.warning_express_no_exchangeable_coins_description, + formatArgs = wrappedList(tokenName), + ), + ) + + data class NeedReserveToCreateAccount( + val amount: String, + val token: String, + ) : Warning( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$amount $token"), + ), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + ) + + data class ReduceAmount( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference( + R.string.send_notification_high_fee_text, + wrappedList(currencyName, amount), + ), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference( + R.string.xtz_withdrawal_message_reduce, + wrappedList(amount), + ), + onClick = onConfirmClick, + ), + ) + + data class ExpressError( + val expressDataError: ExpressDataError, + val onConfirmClick: () -> Unit, + ) : Warning( + title = getExpressErrorTitle(expressDataError), + subtitle = getExpressErrorMessage(expressDataError), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onConfirmClick, + ), + ) + + data class ExpressGeneralError( + val code: Int, + val onConfirmClick: () -> Unit, + ) : Warning( + title = TextReference.Res(R.string.warning_express_refresh_required_title), + subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = TextReference.Res(R.string.warning_button_refresh), + onClick = onConfirmClick, + ), + ) + } + + sealed class Info( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_circle_24, + ) : NotificationUM.Info( + title = title, + subtitle = subtitle, + iconResId = iconResId, + ) { + data class PermissionNeeded( + val providerName: String, + val fromTokenSymbol: String, + ) : Info( + title = resourceReference(R.string.express_provider_permission_needed), + subtitle = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, fromTokenSymbol), + ), + iconResId = R.drawable.ic_locked_24, + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt deleted file mode 100644 index ee2bce3961..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.feature.swap.models.states - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent - -internal data class WebViewBottomSheetConfig(val url: String) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt index 1741d69029..a00fed2610 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt @@ -6,6 +6,4 @@ import com.tangem.common.ui.alerts.models.AlertUM @Immutable internal sealed class SwapEvent { data class ShowAlert(val alert: AlertUM) : SwapEvent() - - data class ShowShareDialog(val txUrl: String) : SwapEvent() } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 60df4a856b..86c77dd6e8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -6,20 +6,17 @@ import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.feature.swap.router.CustomTabsManager import com.tangem.feature.swap.router.SwapNavScreen -import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.SwapScreen import com.tangem.feature.swap.ui.SwapSelectTokenScreen +import com.tangem.feature.swap.ui.SwapStoriesScreen import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.feature.swap.viewmodels.SwapViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject @AndroidEntryPoint @@ -28,20 +25,11 @@ class SwapFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies - @Inject - lateinit var appRouter: AppRouter - private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) - viewModel.setRouter( - SwapRouter( - customTabsManager = CustomTabsManager(WeakReference(context)), - router = appRouter, - ), - ) } @Composable @@ -59,6 +47,14 @@ class SwapFragment : ComposeFragment() { label = "", ) { screen -> when (screen) { + SwapNavScreen.PromoStories -> { + val storiesConfig = viewModel.uiState.storiesConfig + if (storiesConfig != null) { + SwapStoriesScreen(config = storiesConfig) + } else { + SwapScreen(stateHolder = viewModel.uiState) + } + } SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState) SwapNavScreen.Success -> { val successState = viewModel.uiState.successState diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt new file mode 100644 index 0000000000..401f98e20f --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt @@ -0,0 +1,352 @@ +package com.tangem.feature.swap.presentation + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.SwapFeeState +import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.states.FeeItemState +import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold +import com.tangem.lib.crypto.BlockchainUtils.isTezos +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class SwapNotificationsFactory( + private val actions: UiActions, +) { + + fun getInitialErrorStateNotifications(code: Int, onRefreshClick: () -> Unit): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.ExpressGeneralError( + code = code, + onConfirmClick = onRefreshClick, + ), + ) + } + + fun getGeneralErrorStateNotifications( + notifications: ImmutableList, + message: TextReference?, + onClick: () -> Unit, + ): ImmutableList { + val updatedNotifications = notifications.toMutableList() + updatedNotifications.add( + SwapNotificationUM.Error.GenericError( + subtitle = message, + onConfirmClick = onClick, + ), + ) + + return updatedNotifications.toPersistentList() + } + + fun getNotAvailableStateNotifications(fromCurrencyName: String): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.NoAvailableTokensToSwap(fromCurrencyName), + ) + } + + fun getQuotesErrorStateNotifications( + expressDataError: ExpressDataError, + fromToken: CryptoCurrency, + feeItem: FeeItemState, + includeFeeInAmount: IncludeFeeInAmount, + ): ImmutableList { + return buildList { + add(getWarningForError(expressDataError, fromToken, actions.onRetryClick)) + if (includeFeeInAmount is IncludeFeeInAmount.Included && feeItem is FeeItemState.Content) { + add( + NotificationUM.Warning.FeeCoverageNotification( + feeItem.amountCrypto, + feeItem.amountFiatFormatted, + ), + ) + } + }.toPersistentList() + } + + fun getApprovalInProgressStateNotification( + notifications: ImmutableList, + ): ImmutableList { + val updatedNotifications = notifications + .filterNot { it is SwapNotificationUM.Info.PermissionNeeded } + .toMutableList() + + updatedNotifications.add(0, SwapNotificationUM.Error.ApprovalInProgressWarning) + + return updatedNotifications.toPersistentList() + } + + fun getConfirmationStateNotifications( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + selectedFeeType: FeeType, + providerName: String, + ): ImmutableList { + val warnings = buildList { + maybeAddRentExemptionError(quoteModel) + maybeAddDomainWarnings(quoteModel, feeCryptoCurrencyStatus, selectedFeeType) + maybeAddNeedReserveToCreateAccountWarning(quoteModel) + maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName) + maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken) + maybeAddTransactionInProgressWarning(quoteModel) + } + return warnings.toPersistentList() + } + + private fun MutableList.maybeAddRentExemptionError(quoteModel: SwapState.QuotesLoadedState) { + quoteModel.currencyCheck?.rentWarning?.let { + add(NotificationUM.Solana.RentInfo(it)) + } + } + + private fun MutableList.maybeAddTransactionInProgressWarning( + quoteModel: SwapState.QuotesLoadedState, + ) { + if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { + add(SwapNotificationUM.Error.ApprovalInProgressWarning) + } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { + add( + SwapNotificationUM.Error.TransactionInProgressWarning( + currencySymbol = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, + ), + ) + } + } + + private fun MutableList.maybeAddDomainWarnings( + quoteModel: SwapState.QuotesLoadedState, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + selectedFeeType: FeeType, + ) { + val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus + val includeFeeInAmount = quoteModel.preparedSwapConfigState.includeFeeInAmount + val amount = quoteModel.fromTokenInfo.tokenAmount + val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { + includeFeeInAmount.amountSubtractFee + } else { + amount + } + val fee = when (val feeState = quoteModel.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> if (feeState.normalFee.feeType == selectedFeeType) { + feeState.normalFee + } else { + feeState.priorityFee + } + is TxFeeState.SingleFeeState -> feeState.fee + } + val isCardano = BlockchainUtils.isCardano(fromCurrencyStatus.currency.network.id.value) + // blockchain specific + + addExistentialWarningNotification( + existentialDeposit = quoteModel.currencyCheck?.existentialDeposit, + feeAmount = fee?.feeValue.orZero(), + sendingAmount = amount.value, + cryptoCurrencyStatus = fromCurrencyStatus, + onReduceClick = { reduceBy, reduceByDiff, _ -> + actions.onReduceByAmount( + amount.copy(value = amount.value.minus(reduceByDiff)), + reduceBy, + ) + }, + ) + addValidateTransactionNotifications( + dustValue = quoteModel.currencyCheck?.dustValue.orZero(), + validationError = quoteModel.validationResult, + cryptoCurrency = fromCurrencyStatus.currency, + minAdaValue = quoteModel.minAdaValue, + onReduceClick = { reduceTo, _ -> + actions.onReduceToAmount(amount.copy(value = reduceTo)) + }, + ) + if (!isCardano) { + addDustWarningNotification( + dustValue = quoteModel.currencyCheck?.dustValue, + feeValue = fee?.feeValue.orZero(), + sendingAmount = amountToRequest.value, + cryptoCurrencyStatus = fromCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addReserveAmountErrorNotification( + reserveAmount = quoteModel.currencyCheck?.reserveAmount, + sendingAmount = amountToRequest.value, + cryptoCurrency = fromCurrencyStatus.currency, + isAccountFunded = false, + ) + addReduceAmountNotification( + cryptoCurrencyStatus = fromCurrencyStatus, + fromAmount = quoteModel.fromTokenInfo.tokenAmount, + onReduceByAmount = actions.onReduceByAmount, + ) + addTransactionLimitErrorNotification( + utxoLimit = quoteModel.currencyCheck?.utxoAmountLimit, + cryptoCurrency = fromCurrencyStatus.currency, + onReduceClick = { reduceTo, _ -> + actions.onReduceToAmount(amountToRequest.copy(value = reduceTo)) + }, + ) + } + + private fun MutableList.maybeAddNeedReserveToCreateAccountWarning( + quoteModel: SwapState.QuotesLoadedState, + ) { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + val currencyTo = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency + if (amount < amountToCreateAccount) { + add( + SwapNotificationUM.Warning.NeedReserveToCreateAccount( + status.amountToCreateAccount.parseBigDecimal(currencyTo.decimals), + currencyTo.symbol, + ), + ) + } + } + } + + private fun MutableList.maybeAddPermissionNeededWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + providerName: String, + ) { + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + add( + SwapNotificationUM.Info.PermissionNeeded( + providerName = fromToken.symbol, + fromTokenSymbol = providerName, + ), + ) + } + } + + private fun MutableList.maybeAddNetworkFeeCoverageWarning( + quoteModel: SwapState.QuotesLoadedState, + selectedFeeType: FeeType, + ) { + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> { + val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return + if (needShowNetworkFeeCoverageWarningShow(quoteModel)) { + add( + NotificationUM.Warning.FeeCoverageNotification( + fee.feeCryptoFormattedWithNative, + fee.feeFiatFormattedWithNative, + ), + ) + } + } + else -> Unit + } + } + + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { + return when (txFeeState) { + TxFeeState.Empty -> null + is TxFeeState.SingleFeeState -> txFeeState.fee + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + } + } + + private fun MutableList.maybeAddUnableCoverFeeWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + ) { + val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return + val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.permissionState !is PermissionDataState.PermissionLoading && + feeEnoughState.feeCurrency != fromToken + if (needShowCoverWarning) { + add( + SwapNotificationUM.Error.UnableToCoverFeeWarning( + fromToken = fromToken, + feeCurrency = feeEnoughState.feeCurrency, + currencyName = feeEnoughState.currencyName ?: fromToken.network.name, + currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol, + onConfirmClick = actions.onBuyClick, + ), + ) + } + } + + private fun MutableList.addReduceAmountNotification( + cryptoCurrencyStatus: CryptoCurrencyStatus, + fromAmount: SwapAmount, + onReduceByAmount: (SwapAmount, BigDecimal) -> Unit, + ) { + val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value) + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val threshold = getTezosThreshold() + val isTotalBalance = fromAmount.value >= balance && balance > threshold + if (isTezos && isTotalBalance) { + add( + SwapNotificationUM.Warning.ReduceAmount( + currencyName = cryptoCurrencyStatus.currency.name, + amount = threshold.toPlainString(), + onConfirmClick = { + val patchedAmount = fromAmount.copy( + value = fromAmount.value - threshold, + ) + onReduceByAmount(patchedAmount, threshold) + }, + ), + ) + } + } + + private fun getWarningForError( + expressDataError: ExpressDataError, + fromToken: CryptoCurrency, + onRetryClick: () -> Unit, + ): NotificationUM { + return when (expressDataError) { + is ExpressDataError.ExchangeTooSmallAmountError -> SwapNotificationUM.Error.MinimalAmountError( + expressDataError.amount.value.format { + crypto(fromToken.symbol, fromToken.decimals) + }, + ) + is ExpressDataError.ExchangeTooBigAmountError -> SwapNotificationUM.Error.MaximumAmountError( + expressDataError.amount.value.format { + crypto(fromToken.symbol, fromToken.decimals) + }, + ) + else -> SwapNotificationUM.Warning.ExpressError( + expressDataError, + onConfirmClick = onRetryClick, + ) + } + } + + private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean { + return quoteModel.currencyCheck?.existentialDeposit == null + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt deleted file mode 100644 index af04294b72..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.feature.swap.router - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent -import java.lang.ref.WeakReference - -@Deprecated("Replace with CustomTabsUrlOpener") -class CustomTabsManager(private val context: WeakReference) { - - fun openUrl(url: String) { - val mContext = context.get() ?: return - val browserIntent = Intent() - .setAction(Intent.ACTION_VIEW) - .addCategory(Intent.CATEGORY_BROWSABLE) - .setDataAndType(Uri.fromParts("http", "", null), "text/plain") - var possibleBrowsers = - mContext.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) - if (possibleBrowsers.isEmpty()) { - possibleBrowsers = - mContext.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL) - } - if (possibleBrowsers.isNotEmpty()) { - val customTabsIntent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams( - CustomTabColorSchemeParams.Builder().build(), - ) - .build() - customTabsIntent.intent.setPackage(possibleBrowsers[0].activityInfo.packageName) - context.get()?.let { customTabsIntent.launchUrl(it, Uri.parse(url)) } - } else { - val browserIntent2 = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - mContext.startActivity(browserIntent2) - } - } -} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt index c260e1bac4..48bdde9475 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt @@ -9,7 +9,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId internal class SwapRouter( - private val customTabsManager: CustomTabsManager, private val router: AppRouter, ) { @@ -45,10 +44,6 @@ internal class SwapRouter( } } - fun openUrl(url: String) { - customTabsManager.openUrl(url) - } - fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { val route = AppRoute.CurrencyDetails( userWalletId = userWalletId, @@ -72,5 +67,5 @@ internal class SwapRouter( } enum class SwapNavScreen { - Main, Success, SelectToken + Main, Success, SelectToken, PromoStories } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index f183c74cbd..13dc36fbac 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -17,10 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.ui.FeeType @@ -69,7 +65,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { @Composable private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMoreClick: (String) -> Unit) { val linkText = readMore.resolveReference() - val fullString = stringResource(R.string.common_fee_selector_footer, linkText) + val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText) val linkTextPosition = fullString.length - linkText.length val annotatedString = buildAnnotatedString { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { @@ -174,7 +170,7 @@ private fun Preview_ChooseFeeBottomSheet() { TangemThemePreview { ChooseFeeBottomSheet( config = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = content, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 3663417055..44fae2d1f8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -12,13 +12,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig @@ -43,7 +43,7 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = stringResource(R.string.express_choose_providers_subtitle), + text = stringResourceSafe(R.string.express_choose_providers_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, modifier = Modifier @@ -89,7 +89,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC tint = TangemTheme.colors.icon.informative, ) Text( - text = stringResource(R.string.express_more_providers_soon), + text = stringResourceSafe(R.string.express_more_providers_soon), style = TangemTheme.typography.caption2, color = TangemTheme.colors.icon.informative, modifier = Modifier @@ -134,7 +134,7 @@ private fun Preview_ChooseProviderBottomSheet() { TangemThemePreview { ChooseProviderBottomSheet( TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = {}, content = content, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 9bf6689b1c..7e1c08759d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA @@ -122,7 +122,7 @@ private fun ProviderContentState( Row { if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) { Text( - text = stringResource(id = R.string.express_by_provider), + text = stringResourceSafe(id = R.string.express_by_provider), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), @@ -262,7 +262,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxWidth()) { Column { Text( - text = stringResource(R.string.express_provider), + text = stringResourceSafe(R.string.express_provider), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), @@ -282,7 +282,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { strokeWidth = TangemTheme.dimens.size2, ) Text( - text = stringResource(R.string.express_fetch_best_rates), + text = stringResourceSafe(R.string.express_fetch_best_rates), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), @@ -357,7 +357,7 @@ private fun BestTradeItem(modifier: Modifier = Modifier) { ), ) { Text( - text = stringResource(R.string.express_provider_best_rate), + text = stringResourceSafe(R.string.express_provider_best_rate), style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.accent, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), @@ -374,7 +374,7 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { ), ) { Text( - text = stringResource(id = R.string.express_provider_permission_needed), + text = stringResourceSafe(id = R.string.express_provider_permission_needed), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), @@ -391,7 +391,7 @@ private fun RecommendedItem(modifier: Modifier = Modifier) { ), ) { Text( - text = stringResource(id = R.string.express_provider_recommended), + text = stringResourceSafe(id = R.string.express_provider_recommended), style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.accent, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index cc94ac10b4..2cbb63b407 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -4,13 +4,15 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -20,21 +22,24 @@ import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.NetworkInfo +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.presentation.SwapNotificationsFactory import com.tangem.feature.swap.utils.formatToUIRepresentation -import com.tangem.feature.swap.utils.getExpressErrorMessage -import com.tangem.feature.swap.utils.getExpressErrorTitle import com.tangem.feature.swap.viewmodels.SwapProcessDataState import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode @@ -60,6 +65,10 @@ internal class StateBuilder( appCurrencyProvider = appCurrencyProvider, ) + private val notificationsFactory by lazy(LazyThreadSafetyMode.NONE) { + SwapNotificationsFactory(actions) + } + fun createInitialLoadingState( initialCurrencyFrom: CryptoCurrency, initialCurrencyTo: CryptoCurrency?, @@ -71,7 +80,7 @@ internal class StateBuilder( type = TransactionCardType.Inputtable( onAmountChanged = actions.onAmountChanged, onFocusChanged = actions.onAmountSelected, - isError = false, + inputError = TransactionCardType.InputError.Empty, ), amountEquivalent = null, amountTextFieldValue = null, @@ -107,9 +116,32 @@ internal class StateBuilder( onMaxAmountSelected = actions.onMaxAmountSelected, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, + onSelectTokenClick = actions.onSelectTokenClick, + onSuccess = actions.onSuccess, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, priceImpact = PriceImpact.Empty(), + isInsufficientFunds = false, + ) + } + + fun createStoriesState(uiStateHolder: SwapStateHolder): SwapStateHolder { + return uiStateHolder.copy( + // todo stories [REDACTED_TASK_KEY] fix when design is ready + storiesConfig = SwapStoriesContentConfig( + stories = persistentListOf( + SwapStoryConfig( + imageRes = R.drawable.img_card_with_ring, + ), + SwapStoryConfig( + imageRes = R.drawable.ill_businessman_3d, + ), + SwapStoryConfig( + imageRes = R.drawable.ill_one_inch_powered, + ), + ), + onClose = actions.onStoriesClose, + ), ) } @@ -141,18 +173,7 @@ internal class StateBuilder( ), canSelectAnotherToken = true, ), - warnings = listOf( - SwapWarning.NoAvailableTokensToSwap( - notificationConfig = NotificationConfig( - title = resourceReference(R.string.warning_express_no_exchangeable_coins_title), - subtitle = resourceReference( - id = R.string.warning_express_no_exchangeable_coins_description, - formatArgs = wrappedList(fromToken.currency.name), - ), - iconResId = R.drawable.img_attention_20, - ), - ), - ), + notifications = notificationsFactory.getNotAvailableStateNotifications(fromToken.currency.name), fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -174,11 +195,11 @@ internal class StateBuilder( if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder val sendInputType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) - val sendInput = if (sendInputType.isError) { + val sendInput = if (sendInputType.inputError !is TransactionCardType.InputError.Empty) { sendInputType } else { sendInputType.copy( - isError = false, + inputError = TransactionCardType.InputError.Empty, header = TextReference.Res(R.string.swapping_from_title), ) } @@ -211,7 +232,7 @@ internal class StateBuilder( networkIconRes = getActiveIconRes(toToken.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = emptyList(), + notifications = persistentListOf(), fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, onClick = {}), providerState = ProviderState.Loading(), @@ -235,6 +256,7 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, swapProvider: SwapProvider, bestRatedProviderId: String, isNeedBestRateBadge: Boolean, @@ -243,9 +265,10 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = getWarningsForSuccessState( + val notifications = notificationsFactory.getConfirmationStateNotifications( quoteModel = quoteModel, fromToken = fromToken, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, selectedFeeType = selectedFeeType, providerName = swapProvider.name, ) @@ -259,14 +282,22 @@ internal class StateBuilder( TextReference.Res(R.string.swapping_from_title) } val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) - val sendInput = if (sendCardType.isError && !isInsufficientFunds) { - // if any error in inputField and funds enough -> show that error else show fund is not enough error - sendCardType - } else { - sendCardType.copy( - isError = isInsufficientFunds, - header = insufficientFundsHeader, - ) + val sendInput = when (sendCardType.inputError) { + is TransactionCardType.InputError.WrongAmount, + -> sendCardType + is TransactionCardType.InputError.Empty, + is TransactionCardType.InputError.InsufficientFunds, + -> { + val error = if (isInsufficientFunds) { + TransactionCardType.InputError.InsufficientFunds + } else { + TransactionCardType.InputError.Empty + } + sendCardType.copy( + inputError = error, + header = insufficientFundsHeader, + ) + } } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( @@ -304,7 +335,8 @@ internal class StateBuilder( balance = toCurrencyStatus.getFormattedAmount(isNeedSymbol = false), isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = warnings, + isInsufficientFunds = isInsufficientFundsCondition(quoteModel), + notifications = notifications, permissionState = convertPermissionState( lastPermissionState = uiStateHolder.permissionState, permissionDataState = quoteModel.permissionState, @@ -314,7 +346,7 @@ internal class StateBuilder( ), fee = feeState, swapButton = SwapButton( - enabled = getSwapButtonEnabled(quoteModel), + enabled = getSwapButtonEnabled(notifications), onClick = actions.onSwapClick, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -350,294 +382,30 @@ internal class StateBuilder( LegalState( title = resourceReference(R.string.common_terms_of_use), link = it, - onClick = actions.onTosClick, + onClick = actions.onLinkClick, ) }, policyLink = swapProvider.privacyPolicy?.let { LegalState( title = resourceReference(R.string.common_privacy_policy), link = it, - onClick = actions.onPolicyClick, + onClick = actions.onLinkClick, ) }, ) } - @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun getWarningsForSuccessState( - quoteModel: SwapState.QuotesLoadedState, - fromToken: CryptoCurrency, - selectedFeeType: FeeType, - providerName: String, - ): List { - val warnings = mutableListOf() - maybeAddDomainWarnings(quoteModel, warnings) - maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) - maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken, providerName) - maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) - maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) - maybeAddInsufficientFundsWarning(quoteModel, warnings) - maybeAddTransactionInProgressWarning(quoteModel, warnings) - return warnings - } - - private fun maybeAddTransactionInProgressWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { - warnings.add( - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), - ), - ) - } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { - warnings.add( - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_active_transaction_title), - description = resourceReference( - id = R.string.warning_express_active_transaction_message, - formatArgs = wrappedList( - quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, - ), - ), - ), - ) - } - } - - private fun maybeAddDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { - quoteModel.warnings.forEach { - when (it) { - is Warning.ExistentialDepositWarning -> { - warnings.add(createLeaveExistentialDepositWarning(quoteModel, it)) - } - is Warning.MinAmountWarning -> { - warnings.add( - SwapWarning.GeneralError( - NotificationConfig( - title = resourceReference(R.string.send_notification_invalid_amount_title), - subtitle = resourceReference( - R.string.warning_express_dust_message, - wrappedList( - it.dustValue.toPlainString(), - it.dustValue.toPlainString(), - ), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ), - ) - } - is Warning.ReduceAmountWarning -> { - warnings.add( - SwapWarning.ReduceAmount( - notificationConfig = createReduceAmountNotificationConfig( - currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, - amount = it.tezosFeeThreshold.toPlainString(), - onConfirmClick = { - val fromAmount = quoteModel.fromTokenInfo.tokenAmount - val patchedAmount = fromAmount.copy( - value = fromAmount.value - it.tezosFeeThreshold, - ) - actions.onReduceAmount(patchedAmount) - }, - ), - ), - ) - } - Warning.Cardano.InsufficientBalanceToTransferCoin -> { - warnings.add(createInsufficientBalanceToTransferCoin()) - } - is Warning.Cardano.InsufficientBalanceToTransferToken -> { - warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName)) - } - is Warning.Cardano.MinAdaValueCharged -> { - warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName)) - } - } - } - } - - private fun maybeAddNeedReserveToCreateAccountWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value - if (status is CryptoCurrencyStatus.NoAccount) { - val amount = quoteModel.toTokenInfo.tokenAmount.value - val amountToCreateAccount = status.amountToCreateAccount - - if (amount < amountToCreateAccount) { - warnings.add( - SwapWarning.NeedReserveToCreateAccount( - notificationConfig = createActivateAccountNotificationConfig( - status.amountToCreateAccount, - quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.symbol, - ), - ), - ) - } - } - } - - private fun createLeaveExistentialDepositWarning( - quoteModel: SwapState.QuotesLoadedState, - domainWarning: Warning.ExistentialDepositWarning, - ): SwapWarning { - val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency - val deposit = domainWarning.existentialDeposit.format { crypto(fromCurrency).uncapped() } - - return SwapWarning.GeneralError( - NotificationConfig( - title = resourceReference(R.string.send_notification_existential_deposit_title), - subtitle = resourceReference( - R.string.send_notification_existential_deposit_text, - wrappedList(deposit), - ), - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.common_ok), - onClick = { - actions.onLeaveExistentialDeposit( - SwapAmount( - domainWarning.minAvailableAmount, - fromCurrency.decimals, - ), - ) - }, - ), - ), - ) - } - - private fun maybeAddPermissionNeededWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - fromToken: CryptoCurrency, - providerName: String, - ) { - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && - quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest - ) { - warnings.add( - SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol, providerName), - ), - ) - } - } - - private fun maybeAddNetworkFeeCoverageWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - selectedFeeType: FeeType, - ) { - when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> { - val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return - if (needShowNetworkFeeCoverageWarningShow(quoteModel)) { - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig( - fee.feeCryptoFormattedWithNative, - fee.feeFiatFormattedWithNative, - ), - ), - ) - } - } - else -> Unit - } - } - - private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean { - return quoteModel.warnings.none { it is Warning.ExistentialDepositWarning } - } - - private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { - return when (txFeeState) { - TxFeeState.Empty -> null - is TxFeeState.SingleFeeState -> txFeeState.fee - is TxFeeState.MultipleFeeState -> when (feeType) { - FeeType.NORMAL -> txFeeState.normalFee - FeeType.PRIORITY -> txFeeState.priorityFee - } - } - } - - private fun maybeAddUnableCoverFeeWarning( - quoteModel: SwapState.QuotesLoadedState, - fromToken: CryptoCurrency, - warnings: MutableList, - ) { - val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return - val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.permissionState !is PermissionDataState.PermissionLoading && - feeEnoughState.feeCurrency != fromToken - if (needShowCoverWarning) { - warnings.add( - SwapWarning.UnableToCoverFeeWarning( - createUnableToCoverFeeNotificationConfig( - fromToken = fromToken, - feeCurrency = feeEnoughState.feeCurrency, - currencyName = feeEnoughState.currencyName ?: fromToken.network.name, - currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol, - ), - ), - ) - } - } - - private fun maybeAddInsufficientFundsWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (isInsufficientFundsCondition(quoteModel)) { - warnings.add(SwapWarning.InsufficientFunds) - } - } - private fun isInsufficientFundsCondition(quoteModel: SwapState.QuotesLoadedState): Boolean { return !quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included } - private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { - val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value - if (status is CryptoCurrencyStatus.NoAccount) { - val amount = quoteModel.toTokenInfo.tokenAmount.value - val amountToCreateAccount = status.amountToCreateAccount - - if (amount < amountToCreateAccount) { - return false - } - } - - val preparedSwapConfigState = quoteModel.preparedSwapConfigState - // check has has outgoing transaction - if (preparedSwapConfigState.hasOutgoingTransaction) return false - - // check has MinAmountWarning warning - val hasCriticalWarning = quoteModel.warnings.any { - it is Warning.MinAmountWarning || - it is Warning.Cardano.InsufficientBalanceToTransferCoin || - it is Warning.Cardano.InsufficientBalanceToTransferToken || - it is Warning.ExistentialDepositWarning - } - - if (hasCriticalWarning) return false - - return when (preparedSwapConfigState.includeFeeInAmount) { - IncludeFeeInAmount.BalanceNotEnough -> false - IncludeFeeInAmount.Excluded -> - preparedSwapConfigState.isAllowedToSpend && - preparedSwapConfigState.isBalanceEnough && - preparedSwapConfigState.feeState is SwapFeeState.Enough - is IncludeFeeInAmount.Included -> true + private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean { + return notifications.none { + it is SwapNotificationUM.Error || it is NotificationUM.Error || + it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || + it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + it is SwapNotificationUM.Warning.NeedReserveToCreateAccount } } @@ -653,15 +421,13 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = mutableListOf() - warnings.add(getWarningForError(expressDataError, fromToken.cryptoCurrencyStatus.currency)) - if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) { - val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig( - uiStateHolder.fee.amountCrypto, - uiStateHolder.fee.amountFiatFormatted, - ) - warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification)) - } + val notifications = notificationsFactory.getQuotesErrorStateNotifications( + expressDataError = expressDataError, + fromToken = fromToken.cryptoCurrencyStatus.currency, + feeItem = uiStateHolder.fee, + includeFeeInAmount = includeFeeInAmount, + ) + val providerState = getProviderStateForError( swapProvider = swapProvider, fromToken = fromToken.cryptoCurrencyStatus.currency, @@ -700,7 +466,7 @@ internal class StateBuilder( balance = fromToken.cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false), ), receiveCardData = receiveCardData, - warnings = warnings, + notifications = notifications, permissionState = GiveTxPermissionState.Empty, fee = FeeItemState.Empty, swapButton = SwapButton( @@ -754,57 +520,6 @@ internal class StateBuilder( } } - private fun getWarningForError(expressDataError: ExpressDataError, fromToken: CryptoCurrency): SwapWarning { - val providerErrorMessage = getExpressErrorMessage(expressDataError) - val providerErrorTitle = getExpressErrorTitle(expressDataError) - return when (expressDataError) { - is ExpressDataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference( - id = R.string.warning_express_too_minimal_amount_title, - formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)), - ), - subtitle = resourceReference(R.string.warning_express_wrong_amount_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - is ExpressDataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference( - id = R.string.warning_express_too_maximum_amount_title, - formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)), - ), - subtitle = resourceReference(R.string.warning_express_wrong_amount_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - is ExpressDataError.ProviderDifferentAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference(id = R.string.common_error), - subtitle = resourceReference( - R.string.express_error_provider_amount_roundup, - formatArgs = wrappedList( - expressDataError.code, - expressDataError.fromProviderAmount.format { simple(decimals = expressDataError.decimals) }, - ), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - else -> SwapWarning.GeneralWarning( - notificationConfig = NotificationConfig( - title = providerErrorTitle, - subtitle = providerErrorMessage, - iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.warning_button_refresh), - onClick = actions.onRetryClick, - ), - ), - ) - } - } - fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, @@ -843,7 +558,8 @@ internal class StateBuilder( balance = toTokenStatus?.getFormattedAmount(isNeedSymbol = false) ?: DASH_SIGN, isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = emptyList(), + notifications = persistentListOf(), + isInsufficientFunds = false, fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -902,12 +618,12 @@ internal class StateBuilder( crypto(cryptoCurrency = fromToken, ignoreSymbolPosition = true) } (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( - isError = true, + inputError = TransactionCardType.InputError.WrongAmount, header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)), ) ?: uiState.sendCardData.type } else { (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( - isError = false, + inputError = TransactionCardType.InputError.Empty, header = TextReference.Res(R.string.swapping_from_title), ) ?: uiState.sendCardData.type } @@ -1002,19 +718,8 @@ internal class StateBuilder( fun createInitialErrorState(uiState: SwapStateHolder, code: Int, onRefreshClick: () -> Unit): SwapStateHolder { return uiState.copy( - warnings = listOf( - SwapWarning.GeneralWarning( - notificationConfig = NotificationConfig( - title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), - iconResId = R.drawable.ic_alert_triangle_20, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = TextReference.Res(R.string.warning_button_refresh), - onClick = onRefreshClick, - ), - ), - ), - ), + isInsufficientFunds = false, + notifications = notificationsFactory.getInitialErrorStateNotifications(code, onRefreshClick), ) } @@ -1051,17 +756,9 @@ internal class StateBuilder( } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { - val warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }.toMutableList() - warnings.add( - 0, - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), - ), - ) return uiState.copy( permissionState = GiveTxPermissionState.InProgress, - warnings = warnings, + notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications), ) } @@ -1211,17 +908,14 @@ internal class StateBuilder( fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) - fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { - val renewWarnings = uiState.warnings.toMutableList() - renewWarnings.add( - SwapWarning.GenericWarning( + fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { + return uiState.copy( + notifications = notificationsFactory.getGeneralErrorStateNotifications( + notifications = uiState.notifications, message = message, onClick = onClick, ), ) - return uiState.copy( - warnings = renewWarnings, - ) } private fun convertPermissionState( @@ -1272,17 +966,6 @@ internal class StateBuilder( } } - fun showWebViewBottomSheet(uiState: SwapStateHolder, url: String, onDismiss: () -> Unit): SwapStateHolder { - val config = WebViewBottomSheetConfig(url = url) - return uiState.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, - onDismissRequest = onDismiss, - content = config, - ), - ) - } - fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { val permissionState = uiState.permissionState if (permissionState is GiveTxPermissionState.ReadyForRequest) { @@ -1292,7 +975,7 @@ internal class StateBuilder( ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1303,7 +986,7 @@ internal class StateBuilder( fun dismissBottomSheet(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( - bottomSheetConfig = uiState.bottomSheetConfig?.copy(isShow = false), + bottomSheetConfig = uiState.bottomSheetConfig?.copy(isShown = false), ) } @@ -1327,7 +1010,7 @@ internal class StateBuilder( ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1368,21 +1051,6 @@ internal class StateBuilder( } } - fun updateSelectedProvider(uiState: SwapStateHolder, selectedProviderId: String): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig - return if (config != null) { - uiState.copy( - bottomSheetConfig = uiState.bottomSheetConfig.copy( - content = config.copy( - selectedProviderId = selectedProviderId, - ), - ), - ) - } else { - uiState - } - } - fun showSelectFeeBottomSheet( uiState: SwapStateHolder, selectedFee: FeeType, @@ -1401,11 +1069,11 @@ internal class StateBuilder( readMoreUrl = buildReadMoreUrl(), feeItems = txFeeState.toFeeItemState(), readMore = resourceReference(R.string.common_read_more), - onReadMoreClick = actions.onFeeReadMoreClick, + onReadMoreClick = actions.onLinkClick, ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1420,21 +1088,6 @@ internal class StateBuilder( } } - fun updateSelectedFeeBottomSheet(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig - return if (config != null) { - uiState.copy( - bottomSheetConfig = uiState.bottomSheetConfig.copy( - content = config.copy( - selectedFee = selectedFee, - ), - ), - ) - } else { - uiState - } - } - private fun TxFeeState.MultipleFeeState.toFeeItemState(): ImmutableList { return listOf( FeeItemState.Content( @@ -1483,122 +1136,6 @@ internal class StateBuilder( } } - // region warnings - private fun createPermissionNotificationConfig(fromTokenSymbol: String, providerName: String): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.express_provider_permission_needed), - subtitle = resourceReference( - id = R.string.give_permission_swap_subtitle, - formatArgs = wrappedList(providerName, fromTokenSymbol), - ), - iconResId = R.drawable.ic_locked_24, - ) - } - - private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig { - return NotificationConfig( - title = resourceReference( - id = R.string.send_notification_invalid_reserve_amount_title, - formatArgs = wrappedList("$amount $token"), - ), - subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), - iconResId = R.drawable.img_attention_20, - ) - } - - private fun createReduceAmountNotificationConfig( - currencyName: String, - amount: String, - onConfirmClick: () -> Unit, - ): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.send_notification_high_fee_title), - subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), - iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), - onClick = onConfirmClick, - ), - ) - } - - private fun createUnableToCoverFeeNotificationConfig( - fromToken: CryptoCurrency, - feeCurrency: CryptoCurrency?, - currencyName: String, - currencySymbol: String, - ): NotificationConfig { - val buttonState = feeCurrency?.let { - NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)), - onClick = { actions.onBuyClick(it) }, - ) - } - return NotificationConfig( - title = resourceReference( - R.string.warning_express_not_enough_fee_for_token_tx_title, - wrappedList(fromToken.network.name), - ), - subtitle = resourceReference( - R.string.warning_express_not_enough_fee_for_token_tx_description, - wrappedList(currencyName, currencySymbol), - ), - iconResId = fromToken.networkIconResId, - buttonsState = buttonState, - ) - } - - private fun createNetworkFeeCoverageNotificationConfig( - cryptoAmount: String, - fiatAmount: String, - ): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference( - R.string.common_network_fee_warning_content, - wrappedList(cryptoAmount, fiatAmount), - ), - iconResId = R.drawable.img_attention_20, - ) - } - - private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning { - return SwapWarning.Cardano.MinAdaValueCharged( - NotificationConfig( - title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), - subtitle = resourceReference( - id = R.string.cardano_coin_will_be_send_with_token_description, - formatArgs = wrappedList(minAdaValue, tokenName), - ), - iconResId = R.drawable.img_attention_20, - ), - ) - } - - private fun createInsufficientBalanceToTransferCoin(): SwapWarning { - return SwapWarning.Cardano.InsufficientBalanceToTransferCoin( - NotificationConfig( - title = resourceReference(id = R.string.cardano_max_amount_has_token_title), - subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - } - - private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning { - return SwapWarning.Cardano.InsufficientBalanceToTransferToken( - NotificationConfig( - title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), - subtitle = resourceReference( - id = R.string.cardano_insufficient_balance_to_send_token_description, - formatArgs = wrappedList(tokenName), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - } - // end region - private fun getShortAddressValue(fullAddress: String): String { check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) @@ -1712,14 +1249,6 @@ internal class StateBuilder( return amount.format { crypto(symbol, currency.decimals) } } - @Suppress("UnusedPrivateMember") - private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return DASH_SIGN - val appCurrency = appCurrencyProvider() - - return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) - } - private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() @@ -1735,10 +1264,6 @@ internal class StateBuilder( return this.divide(to, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) } - private fun toBigDecimalOrNull(text: String): BigDecimal? { - return text.replace(",", ".").toBigDecimalOrNull() - } - private fun getLocaleName(): String { return if (Locale.getDefault().language == "ru") { RU_LOCALE @@ -1761,6 +1286,7 @@ internal class StateBuilder( private const val MAX_DECIMALS_TO_SHOW = 8 private const val IF_ZERO_DECIMALS_TO_SHOW = 2 private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" - private const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" + private const val FEE_READ_MORE_URL_SECOND_PART = + "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt index 7d5355c7a7..3a7455b5b0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt @@ -1,22 +1,19 @@ package com.tangem.feature.swap.ui import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R @Composable internal fun SwapEventEffect(event: StateEvent) { - val context = LocalContext.current var alertConfig by remember { mutableStateOf(value = null) } val keyboardController = LocalSoftwareKeyboardController.current @@ -35,9 +32,6 @@ internal fun SwapEventEffect(event: StateEvent) { is SwapEvent.ShowAlert -> { alertConfig = value.alert } - is SwapEvent.ShowShareDialog -> { - context.shareText(value.txUrl) - } } }, ) @@ -53,7 +47,7 @@ internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) { }, ) val dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 7379f7ad2a..e474858740 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -6,16 +6,15 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig import com.tangem.feature.swap.presentation.R @Composable @@ -26,7 +25,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { modifier = Modifier.systemBarsPadding(), topBar = { AppBarWithBackButton( - text = stringResource(R.string.common_swap), + text = stringResourceSafe(R.string.common_swap), onBackClick = stateHolder.onBackClicked, iconRes = R.drawable.ic_close_24, ) @@ -40,20 +39,13 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { modifier = Modifier.padding(scaffoldPaddings), ) - stateHolder.bottomSheetConfig?.let { config -> + if (stateHolder.bottomSheetConfig != null) { + val config = stateHolder.bottomSheetConfig + when (config.content) { - is GiveTxPermissionBottomSheetConfig -> { - GiveTxPermissionBottomSheet(config = config) - } - is ChooseProviderBottomSheetConfig -> { - ChooseProviderBottomSheet(config = config) - } - is ChooseFeeBottomSheetConfig -> { - ChooseFeeBottomSheet(config = config) - } - is WebViewBottomSheetConfig -> { - WebViewBottomSheet(config = config) - } + is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(config = config) + is ChooseProviderBottomSheetConfig -> ChooseProviderBottomSheet(config = config) + is ChooseFeeBottomSheetConfig -> ChooseFeeBottomSheet(config = config) } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 54d6940303..deecc7282f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 @@ -24,13 +23,10 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.getActiveIconResByCoinId -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.ui.FeeType @@ -38,7 +34,9 @@ import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R +import kotlinx.collections.immutable.persistentListOf @Suppress("LongMethod") @Composable @@ -69,7 +67,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi FeeItemBlock(state = state.fee) - if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) + if (state.notifications.isNotEmpty()) SwapNotifications(notifications = state.notifications) MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) @@ -84,7 +82,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) { Text( - text = stringResource(id = R.string.send_max_amount_label), + text = stringResourceSafe(id = R.string.send_max_amount_label), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, modifier = Modifier @@ -206,7 +204,7 @@ private fun getAnnotatedStringForLegalsWithClick( return if (tos != null && policy != null) { val tosTitle = tos.title.resolveReference() val policyTitle = policy.title.resolveReference() - val fullString = stringResource(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle) + val fullString = stringResourceSafe(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle) val tosIndex = fullString.indexOf(tosTitle) val policyIndex = fullString.indexOf(policyTitle) val string = buildAnnotatedString { @@ -238,7 +236,7 @@ private fun getAnnotatedStringForLegalsWithClick( val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" } val legalTitle = legal.title .resolveReference() - val fullString = stringResource(id = R.string.express_legal_one_placeholder, legal) + val fullString = stringResourceSafe(id = R.string.express_legal_one_placeholder, legal) val legalIndex = fullString.indexOf(legalTitle) val string = buildAnnotatedString { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { @@ -302,69 +300,27 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -private fun SwapWarnings(warnings: List) { +private fun SwapNotifications(notifications: List) { Column( modifier = Modifier .background(color = TangemTheme.colors.background.secondary) .fillMaxWidth(), ) { - warnings.forEach { warning -> - when (warning) { - is SwapWarning.PermissionNeeded -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GenericWarning -> { - val message = warning.message?.resolveReference() - ?: stringResource(id = R.string.common_unknown_error) + notifications.forEach { notification -> + when (notification) { + is SwapNotificationUM.Error.GenericError -> { RefreshableWarningCard( - title = stringResource(id = R.string.common_warning), - description = message, - onClick = warning.onClick, + title = notification.title.resolveReference(), + description = notification.config.subtitle.resolveReference(), + onClick = notification.onConfirmClick, ) } - is SwapWarning.NoAvailableTokensToSwap -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralError -> { - Notification( - config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.warning, - ) - } - is SwapWarning.UnableToCoverFeeWarning -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralWarning -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralInformational -> { - Notification( - config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.accent, - ) - } - is SwapWarning.NeedReserveToCreateAccount -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.ReduceAmount -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.TransactionInProgressWarning -> { + is SwapNotificationUM.Error.ApprovalInProgressWarning, + is SwapNotificationUM.Error.TransactionInProgressWarning, + -> { CardWithIcon( - title = warning.title.resolveReference(), - description = warning.description.resolveReference(), + title = notification.config.title?.resolveReference().orEmpty(), + description = notification.config.subtitle.resolveReference(), icon = { CircularProgressIndicator( modifier = Modifier @@ -375,8 +331,20 @@ private fun SwapWarnings(warnings: List) { }, ) } - is SwapWarning.Cardano -> Notification(config = warning.notificationConfig) - SwapWarning.InsufficientFunds -> Unit + else -> { + Notification( + config = notification.config, + iconTint = when (notification) { + is SwapNotificationUM.Error.UnableToCoverFeeWarning, + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Info, + is NotificationUM.Warning, + -> null + is NotificationUM.Error -> TangemTheme.colors.icon.warning + }, + ) + } } SpacerH8() } @@ -387,18 +355,18 @@ private fun SwapWarnings(warnings: List) { private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) { // order is important when { - state.warnings.any { it is SwapWarning.InsufficientFunds } -> { + state.isInsufficientFunds -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.swapping_insufficient_funds), + text = stringResourceSafe(id = R.string.swapping_insufficient_funds), enabled = false, onClick = state.swapButton.onClick, ) } - state.warnings.any { it is SwapWarning.PermissionNeeded } -> { + state.notifications.any { it is SwapNotificationUM.Info.PermissionNeeded } -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.give_permission_title), + text = stringResourceSafe(id = R.string.give_permission_title), enabled = true, onClick = onPermissionWarningClick, ) @@ -406,7 +374,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U else -> { PrimaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.swapping_swap_action), + text = stringResourceSafe(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_tangem_24, enabled = state.swapButton.enabled, onClick = state.swapButton.onClick, @@ -418,7 +386,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U // region preview private val sendCard = SwapCardState.SwapCardData( - type = TransactionCardType.Inputtable({}, {}, false), + type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", tokenIconUrl = "", @@ -459,21 +427,12 @@ private val state = SwapStateHolder( isClickable = true, onClick = {}, ), - warnings = listOf( - SwapWarning.PermissionNeeded( - notificationConfig = NotificationConfig( - title = stringReference("Give Permission"), - subtitle = stringReference("To continue swapping you need to give permission to Tangem"), - iconResId = R.drawable.ic_locked_24, - ), - ), - SwapWarning.NoAvailableTokensToSwap( - notificationConfig = NotificationConfig( - title = stringReference("No tokens"), - subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.img_attention_20, - ), + notifications = persistentListOf( + SwapNotificationUM.Info.PermissionNeeded( + providerName = "Provider", + fromTokenSymbol = "POL", ), + SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"), ), swapButton = SwapButton(enabled = true, onClick = {}), onRefresh = {}, @@ -484,6 +443,9 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + isInsufficientFunds = false, + onSuccess = {}, + onSelectTokenClick = {}, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index a0d643fffd..c06a97f04e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH12 @@ -27,6 +26,7 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.SwapSelectTokenStateHolder @@ -60,12 +60,12 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) }, topBar = { ExpandableSearchView( - title = stringResource(R.string.swapping_token_list_title), + title = stringResourceSafe(R.string.swapping_token_list_title), onBackClick = onBack, - placeholderSearchText = stringResource(id = R.string.common_search_tokens), + placeholderSearchText = stringResourceSafe(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = stringResource(id = R.string.express_exchange_token_list_subtitle), + subtitle = stringResourceSafe(id = R.string.express_exchange_token_list_subtitle), ) }, ) @@ -92,7 +92,7 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { .padding(top = TangemTheme.dimens.spacing16) .padding(horizontal = TangemTheme.dimens.spacing30) .align(Alignment.CenterHorizontally), - text = stringResource(id = R.string.exchange_tokens_empty_tokens), + text = stringResourceSafe(id = R.string.exchange_tokens_empty_tokens), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -113,7 +113,7 @@ private fun TokensNotFound(modifier: Modifier = Modifier) { .padding(top = TangemTheme.dimens.spacing32) .padding(horizontal = TangemTheme.dimens.spacing30) .align(Alignment.TopCenter), - text = stringResource(id = R.string.express_token_list_empty_search), + text = stringResourceSafe(id = R.string.express_token_list_empty_search), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapStoriesScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapStoriesScreen.kt new file mode 100644 index 0000000000..edb6c2d5b6 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapStoriesScreen.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.components.stories.StoriesContainer +import com.tangem.feature.swap.models.SwapStoriesContentConfig + +@Composable +internal fun SwapStoriesScreen(config: SwapStoriesContentConfig) { + StoriesContainer( + config = config, + ) { current, _ -> + // todo stories [REDACTED_TASK_KEY] fix when design is ready + Image( + painter = painterResource(current.imageRes), + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index b5c056c643..c1cafdc166 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton @@ -19,6 +18,7 @@ import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -135,7 +135,7 @@ private fun SwapSuccessScreenButtons( if (txUrl.isNotBlank()) { Row { SecondaryButtonIconStart( - text = stringResource(id = R.string.common_explore), + text = stringResourceSafe(id = R.string.common_explore), iconResId = R.drawable.ic_web_24, onClick = onExploreClick, modifier = Modifier.weight(1f), @@ -143,7 +143,7 @@ private fun SwapSuccessScreenButtons( if (showStatusButton) { SpacerW12() SecondaryButtonIconStart( - text = stringResource(id = R.string.express_cex_status_button_title), + text = stringResourceSafe(id = R.string.express_cex_status_button_title), iconResId = R.drawable.ic_arrow_top_right_24, onClick = onStatusClick, modifier = Modifier.weight(1f), @@ -153,7 +153,7 @@ private fun SwapSuccessScreenButtons( SpacerH12() } PrimaryButton( - text = stringResource(id = textRes), + text = stringResourceSafe(id = textRes), enabled = true, onClick = onDoneClick, modifier = Modifier.fillMaxWidth(), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index e6866c7ba9..81a94f3702 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity 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 @@ -38,6 +37,7 @@ import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.ImageBackgroundContrastChecker @@ -74,7 +74,7 @@ fun TransactionCard( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start, ) { - Header(balance = stringResource(R.string.common_balance, balance), type = type) + Header(balance = stringResourceSafe(R.string.common_balance, balance), type = type) Content( type = type, @@ -134,7 +134,7 @@ fun TransactionCardEmpty( horizontalAlignment = Alignment.Start, ) { Header( - balance = stringResource(id = R.string.swapping_token_not_available), + balance = stringResourceSafe(id = R.string.swapping_token_not_available), type = type, ) @@ -186,10 +186,10 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val titleColor = if (type.isError) { - TangemTheme.colors.text.warning - } else { + val titleColor = if (type.inputError is TransactionCardType.InputError.Empty) { TangemTheme.colors.text.tertiary + } else { + TangemTheme.colors.text.warning } Text( text = type.header.resolveReference(), @@ -551,7 +551,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() { @Composable private fun TransactionCardPreview() { TransactionCard( - type = TransactionCardType.Inputtable({}, {}, false), + type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt deleted file mode 100644 index 00a27d8a22..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.feature.swap.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.tooling.preview.Preview -import com.google.accompanist.web.WebView -import com.google.accompanist.web.rememberWebViewState -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig - -@Composable -fun WebViewBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - ) { content: WebViewBottomSheetConfig -> - WebViewBottomSheetContent( - WebViewBottomSheetConfig(url = content.url), - ) - } -} - -@Composable -private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) { - val state = rememberWebViewState(content.url) - val isInPreviewMode = LocalInspectionMode.current - Column( - modifier = Modifier - .fillMaxHeight() - .verticalScroll(rememberScrollState()), - ) { - WebView( - state = state, - modifier = Modifier - .background(TangemTheme.colors.background.secondary), - captureBackPresses = false, - onCreated = { - if (!isInPreviewMode) { - it.settings.apply { - javaScriptEnabled = false - allowFileAccess = false - } - } - }, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_WebViewBottomSheetContent() { - TangemThemePreview { - WebViewBottomSheetContent( - content = WebViewBottomSheetConfig( - url = "https://tangem.com/en/", - ), - ) - } -} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index b35cb05405..3d53f18f52 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -7,13 +7,16 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee +import java.math.BigDecimal data class SwapProcessDataState( // Initial network id val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, + val feePaidCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input val amount: String? = null, + val reduceBalanceBy: BigDecimal = BigDecimal.ZERO, val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapDataModel? = null, val selectedFee: TxFee? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 13de36639d..4ffd6c0123 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -8,12 +8,14 @@ import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter @@ -25,10 +27,8 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -44,13 +44,14 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -81,13 +82,18 @@ internal class SwapViewModel @Inject constructor( private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase, + private val featureToggles: SwapFeatureToggles, swapInteractorFactory: SwapInteractor.Factory, private val savedStateHandle: SavedStateHandle, + private val urlOpener: UrlOpener, + router: AppRouter, ) : ViewModel(), DefaultLifecycleObserver { private val initialCurrencyFrom: CryptoCurrency @@ -142,7 +148,8 @@ internal class SwapViewModel @Inject constructor( // shows currency order (direct - swap initial to selected, reversed = selected to initial) private var isOrderReversed = false private val lastAmount = mutableStateOf(INITIAL_AMOUNT) - private var swapRouter: SwapRouter by Delegates.notNull() + private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) + private var swapRouter: SwapRouter = SwapRouter(router = router) private val isUserResolvableError: (SwapState) -> Boolean = { it is SwapState.SwapError && @@ -165,6 +172,13 @@ internal class SwapViewModel @Inject constructor( val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() } val wallet = getUserWalletUseCase(userWalletId).getOrNull() + val isShowPromoStories = shouldShowSwapStoriesUseCase.invokeSync() && featureToggles.isPromoStoriesEnabled + + if (isShowPromoStories) { + initStories() + swapRouter.openScreen(SwapNavScreen.PromoStories) + } + if (fromStatus == null || wallet == null) { uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) } else { @@ -202,19 +216,6 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) } - fun setRouter(router: SwapRouter) { - swapRouter = router - uiState = uiState.copy( - onSelectTokenClick = { - router.openScreen(SwapNavScreen.SelectToken) - sendSelectTokenScreenOpenedEvent() - }, - onSuccess = { - router.openScreen(SwapNavScreen.Success) - }, - ) - } - private fun sendSelectTokenScreenOpenedEvent() { val isAnyAvailableTokensTo = dataState.tokensDataState?.toGroup?.available?.isNotEmpty() ?: false val isAnyAvailableTokensFrom = dataState.tokensDataState?.fromGroup?.available?.isNotEmpty() ?: false @@ -281,6 +282,10 @@ internal class SwapViewModel @Inject constructor( } } + private fun initStories() { + uiState = stateBuilder.createStoriesState(uiState) + } + private fun applyInitialTokenChoice( state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?, @@ -310,6 +315,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromCurrencyStatus, toToken = toCurrencyStatus, amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus), ) } @@ -327,6 +333,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: String, + reduceBalanceBy: BigDecimal, toProvidersList: List, isSilent: Boolean = false, ) { @@ -345,6 +352,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, + reduceBalanceBy = reduceBalanceBy, toProvidersList = toProvidersList, ), ) @@ -360,6 +368,7 @@ internal class SwapViewModel @Inject constructor( toToken = toCurrency, amount = amount, isSilent = isSilent, + reduceBalanceBy = dataState.reduceBalanceBy, toProvidersList = findSwapProviders(fromCurrency, toCurrency), ) } @@ -369,6 +378,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: String, + reduceBalanceBy: BigDecimal, toProvidersList: List, ): PeriodicTask> { return PeriodicTask( @@ -378,6 +388,7 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, + reduceBalanceBy = reduceBalanceBy, swapDataModel = null, approveDataModel = null, ) @@ -386,6 +397,7 @@ internal class SwapViewModel @Inject constructor( toToken = toToken, providers = toProvidersList, amountToSwap = amount, + reduceBalanceBy = reduceBalanceBy, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } @@ -408,7 +420,7 @@ internal class SwapViewModel @Inject constructor( }, onError = { Timber.e("Error when loading quotes: $it") - uiState = stateBuilder.addWarning(uiState, null) { startLoadingQuotesFromLastState() } + uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) } @@ -423,13 +435,14 @@ internal class SwapViewModel @Inject constructor( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, + feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, swapProvider = provider, bestRatedProviderId = bestRatedProviderId, isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), ) - if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { + if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { analyticsEventHandler.send( SwapEvents.NoticeNotEnoughFee( token = initialCurrencyFrom.symbol, @@ -602,7 +615,7 @@ internal class SwapViewModel @Inject constructor( txUrl = url, onExploreClick = { if (it.txHash.isNotEmpty()) { - swapRouter.openUrl(url) + urlOpener.openUrl(url) } analyticsEventHandler.send( event = SwapEvents.ButtonExplore(initialCurrencyFrom.symbol), @@ -611,7 +624,7 @@ internal class SwapViewModel @Inject constructor( onStatusClick = { val txExternalUrl = it.txExternalUrl if (!txExternalUrl.isNullOrBlank()) { - swapRouter.openUrl(txExternalUrl) + urlOpener.openUrl(txExternalUrl) analyticsEventHandler.send( event = SwapEvents.ButtonStatus(initialCurrencyFrom.symbol), ) @@ -822,6 +835,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromToken, toToken), ) swapRouter.openScreen(SwapNavScreen.Main) @@ -846,6 +860,13 @@ internal class SwapViewModel @Inject constructor( .onEach { Timber.d("${coin.id} balance is ${it.value.amount}") + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = it, + ).getOrNull() ?: it, + ) + uiState = if (isFromCurrency) { dataState = dataState.copy(fromCryptoCurrency = it) stateBuilder.updateSendCurrencyBalance(uiState, it) @@ -884,6 +905,7 @@ internal class SwapViewModel @Inject constructor( ).getOrNull() val decimals = newFromToken.currency.decimals lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) + lastReducedBalanceBy.value = BigDecimal.ZERO uiState = stateBuilder.updateSwapAmount( uiState = uiState, amountFormatted = inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), @@ -895,13 +917,18 @@ internal class SwapViewModel @Inject constructor( fromToken = newFromToken, toToken = newToToken, amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(newFromToken, newToToken), ) } } } - private fun onAmountChanged(value: String) { + private fun onAmountChanged( + value: String, + forceQuotesUpdate: Boolean = false, + reduceBalanceBy: BigDecimal = BigDecimal.ZERO, + ) { viewModelScope.launch { val fromToken = dataState.fromCryptoCurrency val toToken = dataState.toCryptoCurrency @@ -913,6 +940,7 @@ internal class SwapViewModel @Inject constructor( fromToken, ).getOrNull() lastAmount.value = cutValue + lastReducedBalanceBy.value = reduceBalanceBy uiState = stateBuilder.updateSwapAmount( uiState = uiState, amountFormatted = inputNumberFormatter.formatWithThousands(cutValue, decimals), @@ -926,11 +954,12 @@ internal class SwapViewModel @Inject constructor( isAmountChangedByUser = true } - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { + amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY, forceUpdate = forceQuotesUpdate) { startLoadingQuotes( fromToken = fromToken, toToken = toToken, amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromToken, toToken), ) } @@ -946,12 +975,12 @@ internal class SwapViewModel @Inject constructor( } } - private fun onReduceAmountClicked(newAmount: SwapAmount) { - onAmountChanged(newAmount.formatToUIRepresentation()) - } - - private fun onLeaveExistentialDepositClicked(newAmount: SwapAmount) { - onAmountChanged(newAmount.formatToUIRepresentation()) + private fun onReduceAmountClicked(newAmount: SwapAmount, reduceBalanceBy: BigDecimal = BigDecimal.ZERO) { + onAmountChanged( + value = newAmount.formatToUIRepresentation(), + forceQuotesUpdate = true, + reduceBalanceBy = reduceBalanceBy, + ) } private fun onAmountSelected(selected: Boolean) { @@ -1001,7 +1030,7 @@ internal class SwapViewModel @Inject constructor( }, onBackClicked = { val bottomSheet = uiState.bottomSheetConfig - if (bottomSheet != null && bottomSheet.isShow) { + if (bottomSheet != null && bottomSheet.isShown) { uiState = stateBuilder.dismissBottomSheet(uiState) } else { if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { @@ -1012,8 +1041,8 @@ internal class SwapViewModel @Inject constructor( onSearchEntered("") }, onMaxAmountSelected = ::onMaxAmountClicked, - onReduceAmount = ::onReduceAmountClicked, - onLeaveExistentialDeposit = ::onLeaveExistentialDepositClicked, + onReduceToAmount = ::onReduceAmountClicked, + onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) @@ -1040,20 +1069,10 @@ internal class SwapViewModel @Inject constructor( } }, onSelectFeeType = { - val state = dataState.getCurrentLoadedSwapState() ?: return@UiActions - val fromToken = dataState.fromCryptoCurrency ?: return@UiActions - val amountToSwap = dataState.amount ?: return@UiActions - val selectedProvider = dataState.selectedProvider ?: return@UiActions uiState = stateBuilder.dismissBottomSheet(uiState) dataState = dataState.copy(selectedFee = it) viewModelScope.launch(dispatchers.io) { - val updatedState = swapInteractor.updateQuotesStateWithSelectedFee( - state = state, - selectedFee = it.feeType, - fromToken = fromToken, - amountToSwap = amountToSwap, - ) - setupLoadedState(selectedProvider, updatedState, fromToken) + startLoadingQuotesFromLastState(false) } }, onProviderClick = { providerId -> @@ -1090,12 +1109,6 @@ internal class SwapViewModel @Inject constructor( onRetryClick = { startLoadingQuotesFromLastState() }, - onPolicyClick = { - swapRouter.openUrl(it) - }, - onTosClick = { - swapRouter.openUrl(it) - }, onReceiveCardWarningClick = { val selectedProvider = dataState.selectedProvider ?: return@UiActions val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions @@ -1109,8 +1122,16 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.clearAlert(uiState) } }, - onFeeReadMoreClick = { - swapRouter.openUrl(it) + onLinkClick = urlOpener::openUrl, + onSelectTokenClick = { + swapRouter.openScreen(SwapNavScreen.SelectToken) + sendSelectTokenScreenOpenedEvent() + }, + onSuccess = { + swapRouter.openScreen(SwapNavScreen.Success) + }, + onStoriesClose = { + swapRouter.openScreen(SwapNavScreen.Main) }, ) } @@ -1301,7 +1322,9 @@ internal class SwapViewModel @Inject constructor( private fun onFailedTxEmailClick(errorMessage: String) { viewModelScope.launch { - val network = initialFromStatus.currency.network + val transaction = dataState.swapDataModel?.transaction + val fromCurrencyStatus = dataState.fromCryptoCurrency ?: initialFromStatus + val network = fromCurrencyStatus.currency.network val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } saveBlockchainErrorUseCase( @@ -1309,8 +1332,8 @@ internal class SwapViewModel @Inject constructor( errorMessage = errorMessage, blockchainId = network.id.value, derivationPath = network.derivationPath.value, - destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(), - tokenSymbol = initialCurrencyFrom.symbol, + destinationAddress = transaction?.txTo.orEmpty(), + tokenSymbol = fromCurrencyStatus.currency.symbol, amount = dataState.amount.orEmpty(), fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(), ), @@ -1319,7 +1342,7 @@ internal class SwapViewModel @Inject constructor( val email = FeedbackEmailType.SwapProblem( cardInfo = cardInfo, providerName = dataState.selectedProvider?.name.orEmpty(), - txId = dataState.swapDataModel?.transaction?.txId.orEmpty(), + txId = transaction?.txId.orEmpty(), ) sendFeedbackEmailUseCase(email) diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 6897aff661..86afbab205 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -15,14 +15,15 @@ dependencies { implementation(deps.androidx.activity.compose) /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.material3) - implementation(deps.compose.foundation) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.ui.utils) /** DI */ implementation(deps.hilt.android) @@ -52,5 +53,6 @@ dependencies { /** Other modules */ implementation(projects.common.routing) + implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 4ae38f7c83..5a9fbd3fe3 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -23,12 +23,16 @@ import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockc import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel -import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState +import com.tangem.feature.tester.presentation.menu.state.TesterMenuUM +import com.tangem.feature.tester.presentation.menu.state.TesterMenuUM.ButtonUM import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen +import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint +import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject /** Activity for testers */ @@ -70,14 +74,26 @@ internal class TesterActivity : ComposeActivity() { NavHost(navController = navController, startDestination = TesterScreen.MENU.name) { composable(route = TesterScreen.MENU.name) { TesterMenuScreen( - state = TesterMenuContentState( + state = TesterMenuUM( onBackClick = { appRouter.pop { finish() } }, - onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) }, - onEnvironmentTogglesClick = { innerTesterRouter.open(TesterScreen.ENVIRONMENTS_TOGGLES) }, - onExcludedBlockchainsClick = { - innerTesterRouter.open(TesterScreen.EXCLUDED_BLOCKCHAINS) + buttons = persistentSetOf( + ButtonUM.FEATURE_TOGGLES, + ButtonUM.EXCLUDED_BLOCKCHAINS, + ButtonUM.ENVIRONMENT_TOGGLES, + ButtonUM.BLOCKCHAIN_PROVIDERS, + ButtonUM.TESTER_ACTIONS, + ), + onButtonClick = { + val route = when (it) { + ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES + ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS + ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES + ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS + ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS + } + + innerTesterRouter.open(route) }, - onTesterActionsClick = { innerTesterRouter.open(TesterScreen.TESTER_ACTIONS) }, ), ) } @@ -116,6 +132,15 @@ internal class TesterActivity : ComposeActivity() { val state by viewModel.state.collectAsStateWithLifecycle() ExcludedBlockchainsScreen(state = state) } + + composable(route = TesterScreen.BLOCKCHAIN_PROVIDERS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + + val state by viewModel.state.collectAsStateWithLifecycle() + BlockchainProvidersScreen(state) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index 1cc0845d8c..adadf6886f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -8,10 +8,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.apptheme.model.AppThemeMode @@ -27,7 +27,7 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod ) { stickyHeader { AppBarWithBackButton( - text = stringResource(R.string.tester_actions), + text = stringResourceSafe(R.string.tester_actions), onBackClick = state.onBackClick, ) } @@ -36,7 +36,7 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod { (state.hideAllCurrenciesConfig as? HideAllCurrenciesConfig.Clickable)?.onClick?.invoke() ?: Unit } } TesterActionItem( - name = stringResource(R.string.hide_all_currencies), + name = stringResourceSafe(R.string.hide_all_currencies), progress = state.hideAllCurrenciesConfig is HideAllCurrenciesConfig.Progress, onClick = onClick, ) @@ -45,7 +45,7 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod val config = state.toggleAppThemeConfig TesterActionItem( - name = stringResource(id = R.string.toggle_app_theme, config.currentAppTheme.name), + name = stringResourceSafe(id = R.string.toggle_app_theme, config.currentAppTheme.name), onClick = config.onClick, ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefresh.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefresh.kt new file mode 100644 index 0000000000..5c9b785bd2 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefresh.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.tester.presentation.common.components.appbar + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R + +@Composable +internal fun TopBarWithRefresh(state: TopBarWithRefreshUM) { + AppBarWithBackButtonAndIcon( + onBackClick = state.onBackClick, + text = stringResourceSafe(state.titleResId), + iconRes = R.drawable.ic_refresh_24.takeIf { state.refreshButton.isVisible }, + onIconClick = state.refreshButton.onRefreshClick, + backgroundColor = TangemTheme.colors.background.primary, + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefreshUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefreshUM.kt new file mode 100644 index 0000000000..c942dcf83f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/appbar/TopBarWithRefreshUM.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tester.presentation.common.components.appbar + +import androidx.annotation.StringRes + +internal data class TopBarWithRefreshUM( + @StringRes val titleResId: Int, + val onBackClick: () -> Unit, + val refreshButton: RefreshButton, +) { + + data class RefreshButton(val isVisible: Boolean, val onRefreshClick: () -> Unit) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/notification/CustomSetupNotification.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/notification/CustomSetupNotification.kt new file mode 100644 index 0000000000..e2a70bea3b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/common/components/notification/CustomSetupNotification.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tester.presentation.common.components.notification + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.tester.impl.R + +@Composable +internal fun CustomSetupNotification(subtitle: TextReference, modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + subtitle = subtitle, + iconResId = R.drawable.ic_alert_triangle_20, + title = resourceReference(id = R.string.custom_setup_warning_title), + ), + modifier = modifier, + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt index 10a459c2f3..06ad56fc8e 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt @@ -16,11 +16,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.datasource.api.common.config.ApiConfig @@ -45,7 +45,7 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { stickyHeader { AppBarWithBackButton( onBackClick = uiModel.onBackClick, - text = stringResource(id = uiModel.title), + text = stringResourceSafe(id = uiModel.title), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt index 2909504146..772c22afab 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt @@ -114,7 +114,7 @@ private fun TogglesNotification(appVersion: String, isVisible: Boolean, modifier wrappedList(appVersion), ), iconResId = R.drawable.ic_alert_triangle_20, - title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title), + title = resourceReference(id = R.string.custom_setup_warning_title), ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt index fb97f05a55..8ead0deb5e 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -1,42 +1,22 @@ package com.tangem.feature.tester.presentation.featuretoggles.state +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import kotlinx.collections.immutable.ImmutableList /** * Content state of feature toggles screen * - * @property topBarState top bar state + * @property topBar top bar state * @property appVersion app version * @property featureToggles feature toggles list - * @property onBackClick the lambda to be invoked when back button is pressed * @property onToggleValueChange the lambda to be invoked when switch button is pressed * @property onRestartAppClick the lambda to be invoked when restart app button is pressed */ internal data class FeatureTogglesContentState( - val topBarState: TopBarState, + val topBar: TopBarWithRefreshUM, val appVersion: String, val featureToggles: ImmutableList, - val onBackClick: () -> Unit, val onToggleValueChange: (String, Boolean) -> Unit, val onRestartAppClick: () -> Unit, -) { - - /** Top bar state */ - sealed interface TopBarState { - - /** - * Config setup - * - * @property onBackClick the lambda to be invoked when back button is pressed - */ - data object ConfigSetup : TopBarState - - /** - * Custom setup - * - * @property onRecoverClick the lambda to be invoked when recover button is pressed - */ - data class CustomSetup(val onRecoverClick: () -> Unit) : TopBarState - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 6fd2e7e7e9..6db389959c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -10,20 +10,20 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemSwitch -import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefresh +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM +import com.tangem.feature.tester.presentation.common.components.notification.CustomSetupNotification import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import kotlinx.collections.immutable.persistentListOf @@ -41,12 +41,15 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { .fillMaxSize() .background(TangemTheme.colors.background.secondary), ) { - stickyHeader { TopAppBar(state = state.topBarState, onBackClick = state.onBackClick) } + stickyHeader { TopBarWithRefresh(state = state.topBar) } - if (state.topBarState is FeatureTogglesContentState.TopBarState.CustomSetup) { + if (state.topBar.refreshButton.isVisible) { item(key = "warning_notification", contentType = "warning_notification") { - WarningNotification( - appVersion = state.appVersion, + CustomSetupNotification( + subtitle = resourceReference( + id = R.string.feature_toggles_custom_setup_warning_description, + wrappedList(state.appVersion), + ), modifier = Modifier .animateItem() .padding(horizontal = 16.dp) @@ -68,7 +71,7 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { item { PrimaryButton( - text = stringResource(id = R.string.restart_app), + text = stringResourceSafe(id = R.string.restart_app), onClick = state.onRestartAppClick, modifier = Modifier .fillMaxWidth() @@ -78,39 +81,6 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { } } -@Composable -private fun TopAppBar(state: FeatureTogglesContentState.TopBarState, onBackClick: () -> Unit) { - AppBarWithBackButtonAndIcon( - onBackClick = onBackClick, - text = stringResource(id = R.string.feature_toggles), - iconRes = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) { - R.drawable.ic_refresh_24 - } else { - null - }, - onIconClick = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) { - state.onRecoverClick - } else { - null - }, - ) -} - -@Composable -private fun WarningNotification(appVersion: String, modifier: Modifier = Modifier) { - Notification( - config = NotificationConfig( - subtitle = resourceReference( - id = R.string.feature_toggles_custom_setup_warning_description, - wrappedList(appVersion), - ), - iconResId = R.drawable.ic_alert_triangle_20, - title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title), - ), - modifier = modifier, - ) -} - @Composable private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boolean) -> Unit) { Row( @@ -145,17 +115,19 @@ private fun PreviewFeatureTogglesScreen() { FeatureTogglesScreen( state = FeatureTogglesContentState( - topBarState = if (isCustomSetup) { - FeatureTogglesContentState.TopBarState.CustomSetup(onRecoverClick = { isCustomSetup = false }) - } else { - FeatureTogglesContentState.TopBarState.ConfigSetup - }, + topBar = TopBarWithRefreshUM( + titleResId = R.string.feature_toggles, + onBackClick = {}, + refreshButton = TopBarWithRefreshUM.RefreshButton( + isVisible = isCustomSetup, + onRefreshClick = { isCustomSetup = false }, + ), + ), appVersion = "5.15", featureToggles = persistentListOf( TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), ), - onBackClick = {}, onToggleValueChange = { _, _ -> isCustomSetup = true }, onRestartAppClick = {}, ), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 0da5a80bad..95338c3250 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -8,6 +8,8 @@ import androidx.lifecycle.viewModelScope import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter @@ -45,22 +47,40 @@ internal class FeatureTogglesViewModel @Inject constructor( /** Setup navigation state property by router [router] and provides app restart method by [appFinisher] */ fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) { uiState = uiState.copy( - onBackClick = router::back, + topBar = uiState.topBar.copy(onBackClick = router::back), onRestartAppClick = appFinisher::restart, ) } private fun initState(): FeatureTogglesContentState { return FeatureTogglesContentState( - topBarState = getConfigSetupState(), + topBar = getConfigSetupState(isPrimarySetup = true), appVersion = appVersionProvider.versionName, featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), onToggleValueChange = ::onToggleValueChange, - onBackClick = {}, onRestartAppClick = {}, ) } + private fun getConfigSetupState(isPrimarySetup: Boolean): TopBarWithRefreshUM { + val isMatchLocalConfig = mutableFeatureTogglesManager.isMatchLocalConfig() + + return if (isPrimarySetup) { + TopBarWithRefreshUM( + titleResId = R.string.feature_toggles, + onBackClick = {}, + refreshButton = TopBarWithRefreshUM.RefreshButton( + isVisible = !isMatchLocalConfig, + onRefreshClick = ::onRefreshClick, + ), + ) + } else { + uiState.topBar.copy( + refreshButton = uiState.topBar.refreshButton.copy(isVisible = !isMatchLocalConfig), + ) + } + } + private fun onToggleValueChange(name: String, isEnabled: Boolean) { viewModelScope.launch { mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled) @@ -70,25 +90,17 @@ internal class FeatureTogglesViewModel @Inject constructor( // delay for smoothly update animations delay(timeMillis = 300) - uiState = uiState.copy(topBarState = getConfigSetupState()) + uiState = uiState.copy(topBar = getConfigSetupState(isPrimarySetup = false)) } } - private fun getConfigSetupState(): FeatureTogglesContentState.TopBarState { - return if (mutableFeatureTogglesManager.isMatchLocalConfig()) { - FeatureTogglesContentState.TopBarState.ConfigSetup - } else { - FeatureTogglesContentState.TopBarState.CustomSetup( - onRecoverClick = { - viewModelScope.launch { - mutableFeatureTogglesManager.recoverLocalConfig() + private fun onRefreshClick() { + viewModelScope.launch { + mutableFeatureTogglesManager.recoverLocalConfig() - uiState = uiState.copy( - topBarState = getConfigSetupState(), - featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), - ) - } - }, + uiState = uiState.copy( + topBar = getConfigSetupState(isPrimarySetup = false), + featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), ) } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt deleted file mode 100644 index 2dd6061286..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.feature.tester.presentation.menu.state - -/** - * Content state of tester menu screen - * - * @property onBackClick the lambda to be invoked when back button is pressed - * @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed - * @property onEnvironmentTogglesClick the lambda to be invoked when environment toggles button is pressed - * @property onTesterActionsClick the lambda to be invoked when tester actions button is pressed - */ -data class TesterMenuContentState( - val onBackClick: () -> Unit, - val onFeatureTogglesClick: () -> Unit, - val onEnvironmentTogglesClick: () -> Unit, - val onExcludedBlockchainsClick: () -> Unit, - val onTesterActionsClick: () -> Unit, -) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt new file mode 100644 index 0000000000..67750c8620 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.tester.presentation.menu.state + +import androidx.annotation.StringRes +import com.tangem.feature.tester.impl.R +import kotlinx.collections.immutable.ImmutableSet + +/** + * Content state of tester menu screen + * + * @property onBackClick the lambda to be invoked when back button is pressed + * @property buttons list of buttons + * @property onButtonClick the lambda to be invoked when tester button is pressed + */ +data class TesterMenuUM( + val onBackClick: () -> Unit, + val buttons: ImmutableSet, + val onButtonClick: (ButtonUM) -> Unit, +) { + + enum class ButtonUM(@StringRes val textResId: Int) { + FEATURE_TOGGLES(R.string.feature_toggles), + EXCLUDED_BLOCKCHAINS(R.string.excluded_blockchains), + ENVIRONMENT_TOGGLES(R.string.environment_toggles), + BLOCKCHAIN_PROVIDERS(R.string.blockchain_providers), + TESTER_ACTIONS(R.string.tester_actions), + ; + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index dd31fe2f08..da2e67b08d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -2,71 +2,62 @@ package com.tangem.feature.tester.presentation.menu.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R -import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState +import com.tangem.feature.tester.presentation.menu.state.TesterMenuUM +import com.tangem.feature.tester.presentation.menu.state.TesterMenuUM.ButtonUM +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList /** * Screen with functionality for testers * * @param state screen state */ +@OptIn(ExperimentalFoundationApi::class) @Composable -internal fun TesterMenuScreen(state: TesterMenuContentState) { +internal fun TesterMenuScreen(state: TesterMenuUM) { BackHandler(onBack = state.onBackClick) - Column( + LazyColumn( modifier = Modifier .fillMaxSize() - .background(TangemTheme.colors.background.secondary), + .background(TangemTheme.colors.background.primary), ) { - AppBarWithBackButton( - onBackClick = state.onBackClick, - text = stringResource(id = R.string.tester_menu), - ) - - Column( - modifier = Modifier - .padding( - horizontal = TangemTheme.dimens.spacing18, - vertical = TangemTheme.dimens.spacing8, - ) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - ) { - PrimaryButton( - text = stringResource(R.string.feature_toggles), - onClick = state.onFeatureTogglesClick, - modifier = Modifier.fillMaxWidth(), + stickyHeader { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.tester_menu), + containerColor = TangemTheme.colors.background.primary, ) + } + items( + items = state.buttons.toImmutableList(), + key = ButtonUM::textResId, + contentType = { "button" }, + ) { button -> PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.excluded_blockchains), - onClick = state.onExcludedBlockchainsClick, - ) - - PrimaryButton( - text = stringResource(R.string.environment_toggles), - onClick = state.onEnvironmentTogglesClick, - modifier = Modifier.fillMaxWidth(), - ) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.tester_actions), - onClick = state.onTesterActionsClick, + text = stringResourceSafe(button.textResId), + onClick = { state.onButtonClick(button) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), ) } } @@ -78,12 +69,16 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { private fun PreviewTesterMenuScreen() { TangemThemePreview { TesterMenuScreen( - state = TesterMenuContentState( + state = TesterMenuUM( onBackClick = {}, - onFeatureTogglesClick = {}, - onEnvironmentTogglesClick = {}, - onExcludedBlockchainsClick = {}, - onTesterActionsClick = {}, + buttons = persistentSetOf( + ButtonUM.FEATURE_TOGGLES, + ButtonUM.EXCLUDED_BLOCKCHAINS, + ButtonUM.ENVIRONMENT_TOGGLES, + ButtonUM.BLOCKCHAIN_PROVIDERS, + ButtonUM.TESTER_ACTIONS, + ), + onButtonClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 5c501ce531..4e5ba7097d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -11,4 +11,5 @@ internal enum class TesterScreen { ENVIRONMENTS_TOGGLES, TESTER_ACTIONS, EXCLUDED_BLOCKCHAINS, + BLOCKCHAIN_PROVIDERS, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt new file mode 100644 index 0000000000..f63f49d696 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.tester.presentation.providers.entity + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM +import kotlinx.collections.immutable.ImmutableList + +internal data class BlockchainProvidersUM( + val topBar: TopBarWithRefreshUM, + val searchBar: SearchBarUM, + val blockchainProviders: ImmutableList, +) { + + @Immutable + data class ProvidersUM( + val blockchainId: String, + val blockchainName: String, + val blockchainSymbol: String, + val providers: ImmutableList, + val isExpanded: Boolean, + val onDrop: (prev: Int, current: Int) -> Unit, + val addPublicProviderDialog: AddPublicProviderDialogUM, + ) + + @Immutable + data class ProviderUM(val type: ProviderType) { + + val name: String = when (type) { + is ProviderType.Public -> type.url + else -> type::class.java.simpleName + } + } + + data class AddPublicProviderDialogUM( + val hasError: Boolean, + val onValueChange: (String) -> Unit, + val onSaveClick: (String) -> Unit, + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt new file mode 100644 index 0000000000..fab6c7afac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt @@ -0,0 +1,433 @@ +package com.tangem.feature.tester.presentation.providers.ui + +import android.content.ClipData +import android.content.ClipDescription +import android.view.View +import androidx.compose.animation.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.* +import androidx.compose.foundation.draganddrop.dragAndDropSource +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.* +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.rows.RowText +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefresh +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM +import com.tangem.feature.tester.presentation.common.components.notification.CustomSetupNotification +import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM +import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.ProviderUM +import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.ProvidersUM +import kotlinx.collections.immutable.persistentListOf + +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + LazyColumn { + stickyHeader { + Header(topBar = state.topBar, searchBar = state.searchBar) + } + + if (state.topBar.refreshButton.isVisible) { + item(key = "notification", contentType = "notification") { + CustomSetupNotification( + subtitle = resourceReference( + id = R.string.blockchain_providers_custom_setup_warning_description, + ), + modifier = Modifier + .animateItem() + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp), + ) + } + } + + items( + items = state.blockchainProviders, + key = ProvidersUM::blockchainId, + contentType = { "BlockchainProviders" }, + itemContent = { BlockchainProvidersItem(it) }, + ) + } + } +} + +@Composable +private fun Header(topBar: TopBarWithRefreshUM, searchBar: SearchBarUM) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + TopBarWithRefresh(state = topBar) + SearchBar( + state = searchBar, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = TangemTheme.dimens.spacing12), + ) + } +} + +@Composable +private fun BlockchainProvidersItem(state: ProvidersUM) { + var isExpanded by remember { mutableStateOf(state.isExpanded) } + + ExpandableBlockchainRow( + state = state, + content = { Providers(state = state, isExpanded = isExpanded) }, + onExpandStateChange = { isExpanded = !isExpanded }, + ) +} + +@Composable +private fun ExpandableBlockchainRow( + state: ProvidersUM, + content: @Composable () -> Unit, + onExpandStateChange: () -> Unit, +) { + Column { + BlockchainRow(state = state, onExpandStateChange = onExpandStateChange) + + content() + } +} + +@Composable +private fun BlockchainRow(state: ProvidersUM, onExpandStateChange: () -> Unit) { + Row( + modifier = Modifier + .clickable { onExpandStateChange() } + .padding(horizontal = 16.dp) + .fillMaxWidth() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = getActiveIconRes(state.blockchainId)), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size36), + ) + + RowText( + mainText = state.blockchainName, + secondText = state.blockchainSymbol, + accentMainText = true, + accentSecondText = false, + modifier = Modifier.padding(horizontal = 8.dp), + ) + } + + val rotation by animateFloatAsState( + targetValue = if (state.isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} + +@Composable +private fun Providers(state: ProvidersUM, isExpanded: Boolean) { + AnimatedVisibility( + visible = isExpanded, + modifier = Modifier.padding(horizontal = 16.dp), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + label = "providers_visibility", + ) { + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + state.providers.fastForEachIndexed { index, provider -> + key("${provider.name}_$index") { + ProviderItem( + index = index, + state = provider, + onDrop = { prev, new -> state.onDrop(prev, new) }, + ) + } + } + + AddProviderButton(state = state.addPublicProviderDialog) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ProviderItem(index: Int, state: ProviderUM, onDrop: (Int, Int) -> Unit) { + var isActive by remember(index) { mutableStateOf(false) } + val color by animateColorAsState( + targetValue = if (isActive) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.inactive + }, + label = "provider_name_background_color", + ) + + val target = remember { + ProvidersDnDTarget( + index = index, + onActiveStateChange = { isActive = it }, + onDrop = onDrop, + ) + } + + ProviderItemContainer { + ProviderIndex(index = index) + + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(16.dp)) + .padding(vertical = 4.dp, horizontal = 8.dp) + .dragAndDropSource { + detectTapGestures( + onLongPress = { + startTransfer( + DragAndDropTransferData( + clipData = ClipData.newPlainText("provider index", index.toString()), + flags = View.DRAG_FLAG_GLOBAL, + ), + ) + }, + ) + } + .dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event + .mimeTypes() + .contains(ClipDescription.MIMETYPE_TEXT_PLAIN) + }, + target = target, + ), + ) { + Text( + text = state.name, + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun ProviderIndex(index: Int) { + Text( + text = "${index + 1}.", + color = TangemTheme.colors.text.accent, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@Composable +private fun AddProviderButton(state: BlockchainProvidersUM.AddPublicProviderDialogUM) { + var isDialogVisible by remember { mutableStateOf(value = false) } + + ProviderItemContainer { + Box( + modifier = Modifier + .clip(shape = RoundedCornerShape(16.dp)) + .clickable(onClick = { isDialogVisible = true }) + .weight(1f) + .border( + border = BorderStroke(width = 2.dp, color = TangemTheme.colors.icon.accent), + shape = RoundedCornerShape(16.dp), + ) + .padding(horizontal = 8.dp, vertical = 2.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } + + if (isDialogVisible) { + AddPublicProviderDialog( + state = state, + onDismiss = { isDialogVisible = false }, + ) + } +} + +@Composable +private fun AddPublicProviderDialog(state: BlockchainProvidersUM.AddPublicProviderDialogUM, onDismiss: () -> Unit) { + var value by remember { mutableStateOf(value = TextFieldValue()) } + + TextInputDialog( + fieldValue = value, + confirmButton = DialogButtonUM( + title = stringResourceSafe(R.string.common_save), + enabled = value.text.isNotBlank() && !state.hasError, + onClick = { + state.onSaveClick( + buildString { + append(value.text.trim()) + + if (!endsWith("/")) append("/") + }, + ) + onDismiss() + }, + ), + onDismissDialog = onDismiss, + onValueChange = { + value = it + state.onValueChange(it.text) + }, + textFieldParams = AdditionalTextInputDialogUM( + label = "Url", + placeholder = "https://...", + isError = state.hasError, + errorText = "Invalid url", + ), + title = "Public provider", + dismissButton = DialogButtonUM( + title = stringResourceSafe(id = R.string.common_cancel), + onClick = onDismiss, + ), + ) +} + +@Composable +private fun ProviderItemContainer(content: @Composable RowScope.() -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} + +private class ProvidersDnDTarget( + private val index: Int, + private val onActiveStateChange: (Boolean) -> Unit, + private val onDrop: (Int, Int) -> Unit, +) : DragAndDropTarget { + + override fun onEntered(event: DragAndDropEvent) { + super.onEntered(event) + onActiveStateChange(true) + } + + override fun onExited(event: DragAndDropEvent) { + super.onExited(event) + onActiveStateChange(false) + } + + override fun onEnded(event: DragAndDropEvent) { + super.onEnded(event) + onActiveStateChange(false) + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + val new = event.toAndroidDragEvent().clipData.getItemAt(0).text.toString().toInt() + + if (index == new) return false + + onDrop(new, index) + + return true + } +} + +@Preview +@Composable +private fun Preview_BlockchainProvidersScreen() { + TangemThemePreview { + BlockchainProvidersScreen( + state = BlockchainProvidersUM( + topBar = TopBarWithRefreshUM( + titleResId = R.string.blockchain_providers, + onBackClick = {}, + refreshButton = TopBarWithRefreshUM.RefreshButton( + isVisible = true, + onRefreshClick = { }, + ), + ), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + blockchainProviders = persistentListOf( + ProvidersUM( + blockchainId = "ETH", + blockchainName = "Ethereum", + blockchainSymbol = "ETH", + isExpanded = false, + onDrop = { _, _ -> }, + addPublicProviderDialog = BlockchainProvidersUM.AddPublicProviderDialogUM( + hasError = false, + onValueChange = {}, + onSaveClick = {}, + ), + providers = persistentListOf( + ProviderUM(type = ProviderType.NowNodes), + ProviderUM(type = ProviderType.GetBlock), + ), + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt new file mode 100644 index 0000000000..97cf7d9d36 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt @@ -0,0 +1,228 @@ +package com.tangem.feature.tester.presentation.providers.viewmodel + +import androidx.core.util.PatternsCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager +import com.tangem.blockchainsdk.providers.MutableBlockchainProvidersTypesManager +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM +import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.* +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class BlockchainProvidersViewModel @Inject constructor( + blockchainProvidersTypesManager: BlockchainProvidersTypesManager, +) : ViewModel() { + + val state: StateFlow get() = _state.asStateFlow() + + private val _state = MutableStateFlow( + value = BlockchainProvidersUM( + topBar = TopBarWithRefreshUM( + titleResId = R.string.blockchain_providers, + onBackClick = {}, + refreshButton = TopBarWithRefreshUM.RefreshButton(isVisible = false, onRefreshClick = ::onRefreshClick), + ), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = { + searchQuery.value = it.trim() + }, + isActive = false, + onActiveChange = {}, + ), + blockchainProviders = persistentListOf(), + ), + ) + + private val searchQuery = MutableStateFlow(value = "") + + private val manager = blockchainProvidersTypesManager as MutableBlockchainProvidersTypesManager + + init { + subscribeOnBlockchainProviderTypesUpdates() + } + + fun setupNavigation(router: InnerTesterRouter) { + _state.update { state -> + state.copy( + topBar = state.topBar.copy(onBackClick = router::back), + ) + } + } + + private fun subscribeOnBlockchainProviderTypesUpdates() { + combine( + flow = manager.get(), + flow2 = searchQuery, + transform = { blockchainProviderTypes, query -> blockchainProviderTypes to query }, + ) + .onEach { (blockchainProviderTypes, query) -> + val isChanged = !manager.isMatchWithMerged() + + _state.update { state -> + state.copy( + topBar = state.topBar.copy( + refreshButton = state.topBar.refreshButton.copy(isVisible = isChanged), + ), + searchBar = state.searchBar.copy(query = query), + blockchainProviders = blockchainProviderTypes + .filter { + it.key.fullName.contains(other = query, ignoreCase = true) || + it.key.currency.contains(other = query, ignoreCase = true) + } + .toProvidersUM(), + ) + } + } + .launchIn(viewModelScope) + } + + private fun Map>.toProvidersUM(): ImmutableList { + return map { + val (blockchain, providers) = it + + ProvidersUM( + blockchainId = blockchain.id, + blockchainName = blockchain.fullName, + blockchainSymbol = blockchain.currency, + providers = providers.map(BlockchainProvidersUM::ProviderUM).toImmutableList(), + isExpanded = false, + onDrop = { prev, current -> onDrop(blockchain.id, prev, current) }, + addPublicProviderDialog = AddPublicProviderDialogUM( + hasError = false, + onValueChange = { onPublicProviderUrlChange(id = blockchain.id, url = it) }, + onSaveClick = { onAddProviderClick(id = blockchain.id, url = it) }, + ), + ) + } + .sortedBy(ProvidersUM::blockchainName) + .toImmutableList() + } + + private fun onRefreshClick() { + viewModelScope.launch { + manager.update() + } + } + + private fun onDrop(id: String, prev: Int, current: Int) { + val providers = _state.value + .blockchainProviders.first { it.blockchainId == id } + .providers.changeItemOrder(prev, current) + + viewModelScope.launch { + _state.update { + it.copyProvidersUM(blockchainId = id) { + copy(providers = providers.toImmutableList()) + } + } + + manager.update( + blockchain = Blockchain.fromId(id), + providers = providers.map(ProviderUM::type), + ) + } + } + + private fun ImmutableList.changeItemOrder(prev: Int, current: Int): ImmutableList { + return when { + prev > current && prev - current == 1 -> moveUpOneElement(prev, current) + current > prev && current - prev == 1 -> moveDownOneElement(prev, current) + else -> replaceElements(prev, current) + } + .toImmutableList() + } + + // Before: [a, b, c, d, e]; previous = 2 (c), current = 1 (b) + // After: [a, c, b, d, e] + // Calculation: + // 1) [0..current) = [0..1) = [a] + // 2) [list[prev], list[current]] = [list[2], list[1]] = [c, b] + // 3) [(prev+1)..list.size) = [3..5) = [d, e] + // 4) [a] + [c, b] + [d, e] = [a, c, b, d, e] + private fun List.moveUpOneElement(prev: Int, current: Int): List { + return subList(fromIndex = 0, toIndex = current) + + listOf(this[prev], this[current]) + + subList(fromIndex = prev + 1, toIndex = this.size) + } + + // Before: [a, b, c, d, e]; previous = 2 (c), current = 3 (d) + // After: [a, b, d, c, e] + // Calculation: + // 1) [0..prev) = [0..2) = [a, b] + // 2) [list[current], list[prev]] = [list[3], list[2]] = [d, c] + // 3) [(current+1)..list.size) = [4..5) = [e] + // 4) [a, b] + [d, c] + [e] = [a, b, d, c, e] + private fun List.moveDownOneElement(prev: Int, current: Int): List { + return subList(fromIndex = 0, toIndex = prev) + + listOf(this[current], this[prev]) + + subList(fromIndex = current + 1, toIndex = size) + } + + // Before: [a, b, c, d, e]; previous = 0 (a), current = 4 (e) + // After: [e, b, c, d, a] + private fun List.replaceElements(prev: Int, current: Int): List { + val mutableList = toMutableList() + + mutableList[prev] = this[current] + mutableList[current] = this[prev] + + return mutableList + } + + private fun onPublicProviderUrlChange(id: String, url: String) { + _state.update { + it.copyProvidersUM(blockchainId = id) { + copy( + addPublicProviderDialog = addPublicProviderDialog.copy( + hasError = !PatternsCompat.WEB_URL.matcher(url).matches(), + ), + ) + } + } + } + + private fun onAddProviderClick(id: String, url: String) { + val providers = _state.value + .blockchainProviders.first { it.blockchainId == id } + .providers + + viewModelScope.launch { + manager.update( + blockchain = Blockchain.fromId(id), + providers = listOf(ProviderType.Public(url = url)) + providers.map(ProviderUM::type), + ) + } + } + + private fun BlockchainProvidersUM.copyProvidersUM( + blockchainId: String, + update: ProvidersUM.() -> ProvidersUM, + ): BlockchainProvidersUM { + return copy( + blockchainProviders = _state.value.blockchainProviders.map { + if (it.blockchainId == blockchainId) { + it.update() + } else { + it + } + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 95ad57f1ad..77b8a9c63d 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -2,8 +2,9 @@ Tester menu Feature toggles - Custom setup + Custom setup Toggles differs from v%s config + Providers differs from release config Restart app Environment toggles Tester actions @@ -11,4 +12,5 @@ Toggle app theme - %s Excluded blockchains Filter by name or symbol + Blockchain providers diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 58596e7a24..adb8f4302f 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) implementation(deps.androidx.paging.runtime) + implementation(deps.androidx.fragment.ktx) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -83,6 +84,7 @@ dependencies { implementation(projects.domain.markets.models) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) + implementation(projects.domain.promo) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 5b11ac88dd..2c787270d1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -3,8 +3,7 @@ package com.tangem.feature.tokendetails.presentation import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.defaultComponentContext import com.tangem.common.routing.AppRoute @@ -13,6 +12,8 @@ import com.tangem.common.routing.bundle.unbundle import com.tangem.common.routing.utils.asRouter import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment @@ -33,7 +34,11 @@ internal class TokenDetailsFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies @Inject - lateinit var tokenDetailsRouter: TokenDetailsRouter + @GlobalUiMessageSender + internal lateinit var messageSender: UiMessageSender + + @Inject + internal lateinit var tokenDetailsRouter: TokenDetailsRouter @Inject internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider @@ -47,6 +52,8 @@ internal class TokenDetailsFragment : ComposeFragment() { @Inject internal lateinit var appRouter: AppRouter + private val viewModel by viewModels() + private var tokenMarketBlockComponent: TokenMarketBlockComponent? = null private val internalTokenDetailsRouter: InnerTokenDetailsRouter @@ -57,6 +64,9 @@ internal class TokenDetailsFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + viewModel.router = internalTokenDetailsRouter + lifecycle.addObserver(viewModel) + val cryptoCurrency: CryptoCurrency = arguments ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) ?.unbundle(CryptoCurrency.serializer()) @@ -66,7 +76,7 @@ internal class TokenDetailsFragment : ComposeFragment() { val appContext = DefaultAppComponentContext( componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, + messageSender = messageSender, dispatchers = coroutineDispatcherProvider, hiltComponentBuilder = componentBuilder, replaceRouter = appRouter.asRouter(), @@ -86,9 +96,6 @@ internal class TokenDetailsFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val viewModel = hiltViewModel() - viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) NavigationBar3ButtonsScrim() TokenDetailsScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 672da4f66c..83ab6680a0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -90,7 +89,7 @@ internal object TokenDetailsPreviewData { ), ) - val iconState = IconState.TokenIcon( + private val iconState = IconState.TokenIcon( url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", fallbackTint = Color.Cyan, fallbackBackground = Color.Blue, @@ -314,7 +313,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - event = consumedEvent(), ) val tokenDetailsState_2 = TokenDetailsState( @@ -345,7 +343,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - event = consumedEvent(), ) val tokenDetailsState_3 = tokenDetailsState_2.copy( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 06fda7a3b6..ff23815212 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,15 +1,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -29,5 +27,4 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index 8d262a4334..599abba71d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -1,7 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.components import androidx.compose.runtime.Immutable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -9,6 +8,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.components.notifications.CurrencyNotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.model.CryptoCurrency @@ -58,8 +58,8 @@ internal sealed interface ExchangeStatusNotifications { ), subtitle = CurrencyNotificationConfig.AnnotatedSubtitle( valueProvider = { - val linkText = stringResource(R.string.common_read_more) - val fullString = stringResource( + val linkText = stringResourceSafe(R.string.common_read_more) + val fullString = stringResourceSafe( R.string.express_exchange_notification_refund_text, cryptoCurrency.symbol, linkText, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index 9fc0a051b5..3b8d00da73 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -108,23 +108,24 @@ internal class TokenDetailsOnrampTransactionStateConverter( externalTxUrl: String?, providerName: String, ): NotificationUM? { - if (externalTxUrl == null) return null return when (status) { OnrampStatus.Status.Verifying -> { analyticsEventHandler.send(TokenOnrampAnalyticsEvent.NoticeKYC(cryptoCurrency.symbol, providerName)) - ExpressNotificationsUM.NeedVerification { - clickIntents.onGoToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.NeedVerification(onGoToProviderClick = onProviderClick(externalTxUrl)) } OnrampStatus.Status.Failed -> { - ExpressNotificationsUM.FailedByProvider { - clickIntents.onGoToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.FailedByProvider(onGoToProviderClick = onProviderClick(externalTxUrl)) } else -> null } } + private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { + { clickIntents.onGoToProviderClick(externalTxUrl) } + } else { + null + } + private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { return when (status) { OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index f22431609c..6e56b18248 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -3,7 +3,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.getOrElse import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference @@ -78,7 +77,6 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - event = consumedEvent(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 5e15fa4efc..d93b495c65 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -9,8 +9,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -249,12 +247,12 @@ internal class TokenDetailsStateFactory( fun getStateWithReceiveBottomSheet( currency: CryptoCurrency, networkAddress: NetworkAddress, - sendCopyAnalyticsEvent: () -> Unit, - sendShareAnalyticsEvent: () -> Unit, + onCopyClick: (String) -> Unit, + onShareClick: (String) -> Unit, ): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = TokenReceiveBottomSheetConfig( name = currency.name, @@ -262,8 +260,8 @@ internal class TokenDetailsStateFactory( network = currency.network.name, addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = sendCopyAnalyticsEvent, - onShareClick = sendShareAnalyticsEvent, + onCopyClick = onCopyClick, + onShareClick = onShareClick, ), ), ) @@ -275,7 +273,7 @@ internal class TokenDetailsStateFactory( ): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ChooseAddressBottomSheetConfig( addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), @@ -288,7 +286,7 @@ internal class TokenDetailsStateFactory( fun getStateWithClosedBottomSheet(): TokenDetailsState { val state = currentStateProvider() return state.copy( - bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), + bottomSheetConfig = state.bottomSheetConfig?.copy(isShown = false), ) } @@ -321,22 +319,6 @@ internal class TokenDetailsStateFactory( ) } - fun getStateAndTriggerEvent( - state: TokenDetailsState, - errorMessage: TextReference, - setUiState: (TokenDetailsState) -> Unit, - ): TokenDetailsState { - return state.copy( - event = triggeredEvent( - data = errorMessage, - onConsume = { - val currentState = currentStateProvider() - setUiState(currentState.copy(event = consumedEvent())) - }, - ), - ) - } - fun getStateWithUpdatedMenu( cardTypesResolver: CardTypesResolver, hasDerivations: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 7cfbd6eb7b..3c1633c710 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -133,7 +133,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ExpressStatusBottomSheetConfig( value = expressState, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 87ec82270c..0591214019 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -11,12 +11,9 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -32,13 +29,8 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.OkxPromoNotification -import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -62,19 +54,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon BackHandler(onBack = state.topAppBarConfig.onBackClick) val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = bottomBarHeight + TangemTheme.dimens.spacing16, - ), - hostState = snackbarHostState, - ) - }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> @@ -211,23 +192,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon } } } - - TokenDetailsEventEffect(snackbarHostState = snackbarHostState, event = state.event) } } -@Composable -internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event: StateEvent) { - val resources = LocalContext.current.resources - - EventEffect( - event = event, - onTrigger = { value -> - snackbarHostState.showSnackbar(message = value.resolveReference(resources)) - }, - ) -} - // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 54e6b0dc2b..a679980075 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -17,6 +16,7 @@ import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -49,7 +49,7 @@ internal fun TokenDetailsBalanceBlock( val (balanceTitle, toggleButtons, fiatBalance, cryptoBalance, actionChips) = createRefs() Text( - text = stringResource(id = R.string.common_balance_title), + text = stringResourceSafe(id = R.string.common_balance_title), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, modifier = Modifier.constrainAs(balanceTitle) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 9a7a77dd53..350dad39ff 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -62,7 +62,7 @@ private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { when (currency) { TokenInfoBlockState.Currency.Native -> { Text( - text = stringResource(id = R.string.common_main_network), + text = stringResourceSafe(id = R.string.common_main_network), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, ) @@ -104,7 +104,7 @@ private const val SEPARATOR = "%image%" @Composable private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText { val splitString = if (tokenCurrency.standardName != null) { - stringResource( + stringResourceSafe( id = R.string.token_details_token_type_subtitle, formatArgs = arrayOf( tokenCurrency.standardName, @@ -112,7 +112,7 @@ private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): E ), ) } else { - stringResource( + stringResourceSafe( id = R.string.token_details_token_type_subtitle_no_standard, formatArgs = arrayOf( tokenCurrency.networkName, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt index 7b5cd01dd1..7e0091568c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt @@ -17,11 +17,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeStatus @@ -53,7 +53,7 @@ internal fun ExchangeStatusBlock( .padding(bottom = TangemTheme.dimens.spacing16), ) { Text( - text = stringResource(id = R.string.express_exchange_status_title), + text = stringResourceSafe(id = R.string.express_exchange_status_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -72,7 +72,7 @@ internal fun ExchangeStatusBlock( .padding(end = TangemTheme.dimens.spacing2), ) Text( - text = stringResource(id = R.string.common_go_to_provider), + text = stringResourceSafe(id = R.string.common_go_to_provider), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt index ddf1a957dd..3c969fa8d2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.expressStatus.ExpressEstimate import com.tangem.common.ui.expressStatus.ExpressProvider @@ -18,6 +17,7 @@ import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.notifications.CurrencyNotification import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications @@ -28,14 +28,14 @@ internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) { Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( - text = stringResource(id = R.string.express_exchange_status_title), + text = stringResourceSafe(id = R.string.express_exchange_status_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier.align(CenterHorizontally), ) SpacerH10() Text( - text = stringResource(id = R.string.express_exchange_status_subtitle), + text = stringResourceSafe(id = R.string.express_exchange_status_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 76bc8386d6..cd79642bbc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -8,13 +8,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock @@ -37,7 +37,7 @@ internal fun StakingBalanceBlock( modifier = Modifier.weight(1f), ) { Text( - text = stringResource(R.string.staking_native), + text = stringResourceSafe(R.string.staking_native), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingUnavailableBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingUnavailableBlock.kt index 2047a1f09f..067dbeca7a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingUnavailableBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingUnavailableBlock.kt @@ -1,12 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking import android.content.res.Configuration -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.tokendetails.impl.R @@ -18,12 +21,12 @@ internal fun StakingTemporaryUnavailableBlock(modifier: Modifier = Modifier) { modifier = modifier.fillMaxWidth(), ) { Text( - text = stringResource(R.string.staking_native), + text = stringResourceSafe(R.string.staking_native), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) Text( - text = stringResource(R.string.staking_notification_network_error_text), + text = stringResourceSafe(R.string.staking_notification_network_error_text), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 484de66cba..807664c039 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -13,17 +13,19 @@ 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.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.getGreyScaleColorFilter -import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingLoadingBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingTemporaryUnavailableBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM @@ -111,7 +113,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.common_stake), + text = stringResourceSafe(id = R.string.common_stake), enabled = state.isEnabled, onClick = state.onStakeClicked, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index e2446726d6..d3ffbeb33b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -8,10 +8,15 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels @@ -22,6 +27,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -30,8 +36,8 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.model.OnrampSource +import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase @@ -66,10 +72,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory -import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -116,6 +120,8 @@ internal class TokenDetailsViewModel @Inject constructor( private val clipboardManager: ClipboardManager, private val getCryptoCurrencySyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val onrampFeatureToggles: OnrampFeatureToggles, + private val shareManager: ShareManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, @@ -219,6 +225,17 @@ internal class TokenDetailsViewModel @Inject constructor( handleBalanceHiding(owner) } + override fun onPause(owner: LifecycleOwner) { + expressTxStatusTaskScheduler.cancelTask() + expressTxJobHolder.cancel() + super.onPause(owner) + } + + override fun onResume(owner: LifecycleOwner) { + subscribeOnExpressTransactionsUpdates() + super.onResume(owner) + } + override fun onCleared() { expressTxStatusTaskScheduler.cancelTask() expressTxJobHolder.cancel() @@ -544,11 +561,13 @@ internal class TokenDetailsViewModel @Inject constructor( internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, networkAddress = networkAddress, - sendCopyAnalyticsEvent = { + onCopyClick = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) }, - sendShareAnalyticsEvent = { + onShareClick = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) + shareManager.shareText(text = it) }, ) } @@ -581,11 +600,10 @@ internal class TokenDetailsViewModel @Inject constructor( ) if (extendedKey.isNotBlank()) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = extendedKey) - internalUiState.value = stateFactory.getStateAndTriggerEvent( - state = internalUiState.value, - errorMessage = resourceReference(R.string.wallet_notification_address_copied), - setUiState = { internalUiState.value = it }, + clipboardManager.setText(text = extendedKey, isSensitive = true) + + uiMessageSender.send( + message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)), ) } } @@ -683,17 +701,14 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun showErrorIfDemoModeOrElse(action: () -> Unit) { - viewModelScope.launch(dispatchers.main) { - if (isDemoCardUseCase(cardId = userWallet.cardId)) { - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() - internalUiState.value = stateFactory.getStateAndTriggerEvent( - state = internalUiState.value, - errorMessage = resourceReference(id = R.string.alert_demo_feature_disabled), - setUiState = { internalUiState.value = it }, - ) - } else { - action() - } + if (isDemoCardUseCase(cardId = userWallet.cardId)) { + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + + uiMessageSender.send( + message = SnackbarMessage(message = resourceReference(R.string.alert_demo_feature_disabled)), + ) + } else { + action() } } @@ -819,7 +834,7 @@ internal class TokenDetailsViewModel @Inject constructor( val defaultAddress = addresses.firstOrNull()?.value ?: return null vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 0927a822e4..7952d3d3a1 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -1,6 +1,5 @@ package com.tangem.feature.walletsettings.model -import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -12,10 +11,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO @@ -93,26 +91,19 @@ internal class WalletSettingsModel @Inject constructor( isRenameWalletAvailable = isRenameWalletAvailable, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, forgetWallet = { - messageSender.send( - ContentMessage { onDismiss -> - BasicDialog( - message = stringResource(R.string.user_wallet_list_delete_prompt), - onDismissDialog = onDismiss, - confirmButton = DialogButtonUM( - title = stringResource(R.string.common_delete), - warning = true, - onClick = { - forgetWallet() - onDismiss() - }, - ), - dismissButton = DialogButtonUM( - title = stringResource(R.string.common_cancel), - onClick = onDismiss, - ), + val message = DialogMessage( + message = resourceReference(R.string.user_wallet_list_delete_prompt), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_delete), + warning = true, + onClick = ::forgetWallet, ) }, + secondActionBuilder = { cancelAction() }, ) + + messageSender.send(message) }, onLinkMoreCardsClick = { onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 8cbce3fc72..0e6313e933 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -4,11 +4,11 @@ import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent import com.tangem.feature.walletsettings.entity.RenameWalletUM @@ -19,21 +19,21 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { val value by rememberUpdatedState(newValue = model.walletNameValue) TextInputDialog( - title = stringResource(id = R.string.user_wallet_list_rename_popup_title), + title = stringResourceSafe(id = R.string.user_wallet_list_rename_popup_title), fieldValue = value, confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, onValueChange = model.updateValue, textFieldParams = AdditionalTextInputDialogUM( - label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), + label = stringResourceSafe(id = R.string.user_wallet_list_rename_popup_placeholder), ), ) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index f3a06473f2..b2c0270b20 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -12,16 +12,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockItem -import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags @@ -43,12 +41,6 @@ internal fun WalletSettingsScreen( Scaffold( modifier = modifier, containerColor = backgroundColor, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - hostState = LocalSnackbarHostState.current, - ) - }, topBar = { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), @@ -57,7 +49,9 @@ internal fun WalletSettingsScreen( }, content = { paddingValues -> Content( - modifier = Modifier.padding(paddingValues).testTag(TestTags.WALLET_SETTINGS_SCREEN), + modifier = Modifier + .padding(paddingValues) + .testTag(TestTags.WALLET_SETTINGS_SCREEN), state = state, ) @@ -79,7 +73,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { item { Text( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - text = stringResource(id = R.string.wallet_settings_title), + text = stringResourceSafe(id = R.string.wallet_settings_title), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 229ccc95ae..fcae2b21f7 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { implementation(projects.core.deepLinks.global) implementation(projects.core.decompose) implementation(projects.core.datasource) - + implementation(projects.core.res) implementation(projects.libs.crypto) implementation(projects.libs.blockchainSdk) @@ -84,6 +84,8 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.onramp.models) implementation(projects.domain.onramp) + implementation(projects.domain.promo) + implementation(projects.domain.promo.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index ba3c83a69c..11842f32eb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -9,6 +9,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.routing.utils.asRouter import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -45,6 +47,10 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var appRouter: AppRouter + @Inject + @GlobalUiMessageSender + internal lateinit var messageSender: UiMessageSender + private lateinit var marketsEntryComponent: MarketsEntryComponent private val _walletRouter: InnerWalletRouter @@ -57,7 +63,7 @@ internal class WalletFragment : ComposeFragment() { val appContext = DefaultAppComponentContext( componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, + messageSender = messageSender, dispatchers = coroutineDispatcherProvider, hiltComponentBuilder = componentBuilder, replaceRouter = appRouter.asRouter(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 129a6ff281..0f2e5c8932 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -180,7 +180,7 @@ internal object WalletPreviewData { val bottomSheet by lazy { TangemBottomSheetConfig( - isShow = false, + isShown = false, onDismissRequest = {}, content = WalletBottomSheetConfig.UnlockWallets( onUnlockClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index fcff8bb806..614ce24018 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -33,15 +33,9 @@ internal object WalletScreenPreviewData { private val textContentTokensState = WalletTokensListState.ContentState.Content( items = persistentListOf( - TokensListItemUM.NetworkGroupTitle( - id = 1, - name = stringReference("Bitcoin"), - ), + TokensListItemUM.GroupTitle(id = 1, text = stringReference("Network Bitcoin")), TokensListItemUM.Token(state = tokenItemState), - TokensListItemUM.NetworkGroupTitle( - id = 2, - name = stringReference("Ethereum"), - ), + TokensListItemUM.GroupTitle(id = 2, text = stringReference("Network Ethereum")), TokensListItemUM.Token( state = tokenItemState.copy( id = "2", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 1c79fc38cd..9a09f58f26 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -32,9 +31,10 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.tokenlist.internal.DraggableNetworkTitleItem +import com.tangem.core.ui.components.tokenlist.internal.DraggableGroupTitleItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.reordarable.ReorderableItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -176,10 +176,10 @@ private fun LazyItemScope.DraggableItem( val modifierWithBackground = itemModifier.background(color = TangemTheme.colors.background.primary) when (item) { - is DraggableItem.GroupHeader -> DraggableNetworkTitleItem( - modifier = modifierWithBackground, - networkName = item.networkName, + is DraggableItem.GroupHeader -> DraggableGroupTitleItem( + state = item.groupTitle, reorderableTokenListState = reorderableState, + modifier = modifierWithBackground, ) is DraggableItem.Token -> TokenItem( modifier = modifierWithBackground, @@ -232,7 +232,7 @@ private fun TopBar( contentAlignment = Alignment.CenterStart, ) { Text( - text = stringResource(id = R.string.organize_tokens_title), + text = stringResourceSafe(id = R.string.organize_tokens_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, maxLines = 1, @@ -287,12 +287,12 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie ) { SecondaryButton( modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_cancel), + text = stringResourceSafe(id = R.string.common_cancel), onClick = config.onCancelClick, ) PrimaryButton( modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_apply), + text = stringResourceSafe(id = R.string.common_apply), onClick = config.onApplyClick, showProgress = config.showApplyProgress, enabled = config.canApply, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index 37387e16c8..b3443ebd3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -2,6 +2,10 @@ package com.tangem.feature.wallet.presentation.organizetokens.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode /** @@ -30,7 +34,16 @@ internal sealed class DraggableItem { val networkName: String, override val roundingMode: RoundingMode = RoundingMode.None, override val showShadow: Boolean = false, - ) : DraggableItem() + ) : DraggableItem() { + + val groupTitle = TokensListItemUM.GroupTitle( + id = id, + text = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(networkName), + ), + ) + } /** * Item for token. diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index ecc6383e95..15ca6ab348 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -3,14 +3,18 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.it import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import java.math.BigDecimal internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, @@ -57,8 +61,11 @@ internal class CryptoCurrencyToDraggableItemConverter( } private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO + val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance()?.multiply(fiatRate) ?: BigDecimal.ZERO - return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 3bf1f70562..b852180742 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -4,14 +4,14 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.promo.PromoBanner +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.ShouldShowRingPromoUseCase +import com.tangem.domain.promo.models.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.settings.ShouldShowRingPromoUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.repository.PromoRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 8387de10c1..7686df2258 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -67,7 +67,7 @@ internal class OnrampStatusFactory @Inject constructor( analyticsEventHandler.send( TokenOnrampAnalyticsEvent.OnrampStatusChanged( tokenSymbol = onrampTx.info.toAmountSymbol, - status = status.name, + status = status.value, provider = onrampTx.providerName, fiatCurrency = onrampTx.fromCurrencyCode, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 63855cabc5..5c8ab22a90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -98,22 +98,6 @@ internal sealed interface WalletAlertState { override val onConfirmClick: (() -> Unit)? = null } - data object SellingRegionalRestriction : Basic() { - override val title: TextReference = resourceReference(id = R.string.selling_regional_restriction_alert_title) - override val message: TextReference = - resourceReference(id = R.string.selling_regional_restriction_alert_message) - - override val onConfirmClick: (() -> Unit)? = null - } - - data object InsufficientBalanceForSelling : Basic() { - override val title: TextReference = resourceReference(id = R.string.selling_insufficient_balance_alert_title) - override val message: TextReference = - resourceReference(id = R.string.selling_insufficient_balance_alert_message) - - override val onConfirmClick: (() -> Unit)? = null - } - data object InsufficientTokensCountForSwapping : Basic() { override val title: TextReference = resourceReference(id = R.string.action_buttons_swap_no_tokens_added_alert_title) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 50a1af41c3..1b61007fbb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -12,7 +12,7 @@ internal sealed class WalletEvent { data class ShowAlert(val state: WalletAlertState) : WalletEvent() - data class CopyAddress(val address: String) : WalletEvent() + data object CopyAddress : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index 8dddf4cecc..196ec4d3ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -3,7 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.feature.wallet.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -30,7 +31,13 @@ internal sealed class WalletTokensListState { data object Locked : ContentState() { override val items = persistentListOf( - TokensListItemUM.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemUM.GroupTitle( + id = 42, + text = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(resourceReference(id = R.string.main_tokens)), + ), + ), TokensListItemUM.Token(state = TokenItemState.Locked(id = "Locked#1")), ) override val organizeTokensButtonConfig = null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt index 02dd3d93aa..feeea1a264 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt @@ -29,6 +29,6 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS } private fun updateConfig(prevState: WalletState) = prevState.bottomSheetConfig?.copy( - isShow = false, + isShown = false, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt index 69417c564c..46ffbf9066 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt @@ -35,7 +35,7 @@ internal class OpenBottomSheetTransformer( } private fun updateConfig() = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismissBottomSheet, content = content, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index f6443b6ed5..606d0ed064 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -117,25 +117,30 @@ internal class SingleWalletOnrampTransactionConverter( externalTxUrl: String?, providerName: String, ): NotificationUM? { - if (externalTxUrl == null) return null return when (status) { OnrampStatus.Status.Verifying -> { analyticsEventHandler.send( TokenOnrampAnalyticsEvent.NoticeKYC(currency.symbol, providerName), ) - ExpressNotificationsUM.NeedVerification { - clickIntents.onGoToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.NeedVerification( + onGoToProviderClick = onProviderClick(externalTxUrl), + ) } OnrampStatus.Status.Failed -> { - ExpressNotificationsUM.FailedByProvider { - clickIntents.onGoToProviderClick(externalTxUrl) - } + ExpressNotificationsUM.FailedByProvider( + onGoToProviderClick = onProviderClick(externalTxUrl), + ) } else -> null } } + private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { + { clickIntents.onGoToProviderClick(externalTxUrl) } + } else { + null + } + private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { return when (status) { OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 11f5063820..c3d4c02d9c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -2,7 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -10,6 +11,7 @@ import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.converter.Converter @@ -60,9 +62,12 @@ internal class TokenListStateConverter( } private fun MutableList.addGroup(group: NetworkGroup): List { - val groupTitle = TokensListItemUM.NetworkGroupTitle( + val groupTitle = TokensListItemUM.GroupTitle( id = group.network.hashCode(), - name = stringReference(group.network.name), + text = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(group.network.name), + ), ) add(groupTitle) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index f323d5ed9a..de71152155 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -1,13 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.runtime.* -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState @@ -48,7 +48,7 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { }, ) dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) } else { @@ -88,7 +88,7 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U onDismissDialog = onDismiss, onValueChange = { value = it }, title = state.title.resolveReference(), - dismissButton = DialogButtonUM(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + dismissButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss), textFieldParams = AdditionalTextInputDialogUM( label = state.label.resolveReference(), isError = state.errorTextProvider(value.text) != null, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 5bf812ae5e..f9cee1b0a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -5,9 +5,8 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.res.getStringSafe import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -32,7 +31,6 @@ internal fun WalletEventEffect( val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val resources = LocalContext.current.resources - val clipboardManager = LocalClipboardManager.current EventEffect( event = event, onTrigger = { value -> @@ -46,9 +44,8 @@ internal fun WalletEventEffect( snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) } is WalletEvent.CopyAddress -> { - clipboardManager.setText(AnnotatedString(value.address)) snackbarHostState.showSnackbar( - message = resources.getString(R.string.wallet_notification_address_copied), + message = resources.getStringSafe(R.string.wallet_notification_address_copied), duration = SnackbarDuration.Short, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 52a0fea363..6a8f8eb2bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -61,6 +60,7 @@ import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalWindowSize @@ -513,7 +513,7 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.markets_hint), + text = stringResourceSafe(R.string.markets_hint), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -561,12 +561,12 @@ private fun MarketsTooltipContent(modifier: Modifier = Modifier) { horizontalAlignment = Alignment.Start, ) { Text( - text = stringResource(id = R.string.markets_tooltip_title), + text = stringResourceSafe(id = R.string.markets_tooltip_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, ) Text( - text = stringResource(id = R.string.markets_tooltip_message), + text = stringResourceSafe(id = R.string.markets_tooltip_message), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index ec20d2a8d6..eb10376318 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton @@ -19,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.showcase.ShowcaseContent import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.requestPushPermission @@ -76,7 +76,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC ), ) { SecondaryButton( - text = stringResource(R.string.common_later), + text = stringResourceSafe(R.string.common_later), onClick = { content.onNeverRequest() onDismiss() @@ -84,7 +84,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC modifier = Modifier.weight(1f), ) PrimaryButton( - text = stringResource(R.string.common_allow), + text = stringResourceSafe(R.string.common_allow), onClick = { isClicked.value = true content.onRequest() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 6ceb82e75c..9e06c5bfcf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -48,6 +47,7 @@ import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -252,7 +252,7 @@ private fun ManageWalletContextMenu( @Composable private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { DropdownMenuItem( - text = { Text(text = stringResource(id = textResId), style = TangemTheme.typography.subtitle2) }, + text = { Text(text = stringResourceSafe(id = textResId), style = TangemTheme.typography.subtitle2) }, modifier = Modifier.background(color = TangemTheme.colors.background.secondary), trailingIcon = { Icon(imageVector = imageVector, contentDescription = null) }, onClick = onClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 27f0d3b298..dd27fcfa85 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -11,11 +11,11 @@ import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState @@ -91,7 +91,7 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { ) Text( - text = stringResource(id = R.string.main_empty_tokens_list_message), + text = stringResourceSafe(id = R.string.main_empty_tokens_list_message), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 82f79382d6..1d56ea3290 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -79,6 +79,7 @@ internal class WalletViewModel @Inject constructor( private lateinit var router: InnerWalletRouter private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() + private val expressStatusJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -233,10 +234,16 @@ internal class WalletViewModel @Inject constructor( private fun subscribeToScreenBackgroundState() { screenLifecycleProvider.isBackgroundState .onEach { isBackground -> + expressTxStatusTaskScheduler.cancelTask() + expressStatusJobHolder.cancel() refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> triggerRefreshWalletQuotes() + needToRefreshWallet && !isBackground -> { + triggerRefreshWalletQuotes() + subscribeOnExpressTransactionsUpdates() + } + !isBackground -> subscribeOnExpressTransactionsUpdates() } } .launchIn(viewModelScope) @@ -257,7 +264,7 @@ internal class WalletViewModel @Inject constructor( onError = { /* no-op */ }, ), ) - } + }.saveIn(expressStatusJobHolder) } private fun needToRefreshTimer() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 7abe409e3a..db4d42f963 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -7,6 +7,7 @@ import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig @@ -28,8 +29,6 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase @@ -45,7 +44,10 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.features.onramp.OnrampFeatureToggles @@ -113,9 +115,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, - private val getUserCountryUseCase: GetUserCountryUseCase, private val onrampFeatureToggles: OnrampFeatureToggles, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { @@ -167,7 +169,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val defaultAddress = addresses.firstOrNull()?.value ?: return null vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) } @@ -184,9 +186,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) }, onShareClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + shareManager.shareText(text = it) }, ) } @@ -200,12 +204,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( walletManagersFacade.getDefaultAddress( userWalletId = stateHolder.getSelectedWalletId(), network = cryptoCurrencyStatus.currency.network, - )?.let { + )?.let { address -> stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) - walletEventSender.send( - event = WalletEvent.CopyAddress(address = it), - ) + clipboardManager.setText(text = address, isSensitive = true) + walletEventSender.send(event = WalletEvent.CopyAddress) } } } @@ -311,16 +314,26 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - showErrorIfDemoModeOrElse { - viewModelScope.launch(dispatchers.main) { - reduxStateHolder.dispatch( - TradeCryptoAction.Buy( - userWallet = userWallet, - source = OnrampSource.TOKEN_LONG_TAP, - cryptoCurrencyStatus = cryptoCurrencyStatus, - appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, - ), - ) + if (onrampFeatureToggles.isFeatureEnabled) { + appRouter.push( + AppRoute.Onramp( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + source = OnrampSource.TOKEN_LONG_TAP, + ), + ) + } else { + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = userWallet, + source = OnrampSource.TOKEN_LONG_TAP, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } } } } @@ -397,33 +410,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onMultiWalletSellClick(userWalletId: UserWalletId) { - viewModelScope.launch { - val userCountry = getUserCountryUseCase().getOrNull() - if (userCountry is UserCountry.Russia) { - handleError( - alertState = WalletAlertState.SellingRegionalRestriction, - eventCreator = MainScreenAnalyticsEvent::ButtonSell, - ) - - return@launch - } - - val selectedWallet = stateHolder.getSelectedWallet().walletCardState as? WalletCardState.Content - if (selectedWallet?.isZeroBalance == true) { - handleError( - alertState = WalletAlertState.InsufficientBalanceForSelling, - eventCreator = MainScreenAnalyticsEvent::ButtonSell, - ) - - return@launch - } - - onMultiWalletActionClick( - statusFlow = rampStateManager.getSellInitializationStatus(), - route = AppRoute.SellCrypto(userWalletId = userWalletId), - eventCreator = MainScreenAnalyticsEvent::ButtonSell, - ) - } + onMultiWalletActionClick( + statusFlow = rampStateManager.getSellInitializationStatus(), + route = AppRoute.SellCrypto(userWalletId = userWalletId), + eventCreator = MainScreenAnalyticsEvent::ButtonSell, + ) } override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index cf1994333a..ee807b127a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -11,12 +11,12 @@ import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.promo.ShouldShowRingPromoUseCase +import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase -import com.tangem.domain.settings.ShouldShowRingPromoUseCase -import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.model.CryptoCurrency diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7cf4a1e373..3ecbbb9cf6 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -16,7 +16,7 @@ androidxBrowser = "1.4.0" androidxConstraintLayout = "2.1.4" androidxKtx = "1.9.0" androidxSplashScreen = "1.0.1" -androidxFragment = "1.5.3" +androidxFragment = "1.8.5" androidxLifecycle = "2.5.1" androidx-paging = "3.1.1" androidx-palette = "1.0.0" @@ -88,9 +88,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.19-901" +tangemBlockchainSdk = "release-app_5.20-908" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.19-419" +tangemCardSdk = "release-app_5.20-422" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem17" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ @@ -143,6 +143,7 @@ androidx-constraintLayout = { module = "androidx.constraintlayout:constraintlayo androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxKtx" } androidx-core-splashScreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplashScreen" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragment" } +androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "androidxFragment" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } androidx-swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayout" } androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } @@ -196,7 +197,7 @@ tangem-vico-core = { group = "com.tangem.vico", name = "core", version.ref = "ta # region Detekt detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -# ndregion Detekt +# endregion Detekt # region Test test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutine" } @@ -247,7 +248,6 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } -mviCore-watcher = { module = "com.github.badoo.mvicore:mvicore-diff", version.ref = "mviCore" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 741f900b6d..f4ca59e975 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -1,11 +1,7 @@ package com.tangem.blockchainsdk -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.blockchain.common.network.providers.ProviderType -import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter -import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader -import com.tangem.blockchainsdk.store.RuntimeStore +import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -13,25 +9,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber internal typealias BlockchainProvidersResponse = Map> -internal typealias BlockchainProviderTypes = Map> /** * Implementation of Blockchain SDK components factory * - * @property blockchainProvidersResponseLoader blockchain providers response loader - * @property environmentConfigStorage environment config storage - * @property blockchainProviderTypesStore blockchain provider types store - * @property walletManagerFactoryCreator wallet manager factory creator + * @property blockchainProvidersTypesManager blockchain providers types manager + * @property environmentConfigStorage environment config storage + * @property walletManagerFactoryCreator wallet manager factory creator + * @param dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultBlockchainSDKFactory( - private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager, private val environmentConfigStorage: EnvironmentConfigStorage, - private val blockchainProviderTypesStore: RuntimeStore, private val walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKFactory { @@ -42,7 +35,7 @@ internal class DefaultBlockchainSDKFactory( override suspend fun init() { coroutineScope { - updateBlockchainProviderTypes() + launch { blockchainProvidersTypesManager.update() } } } @@ -51,28 +44,11 @@ internal class DefaultBlockchainSDKFactory( private fun createWalletManagerFactory(): Flow { return combine( flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig }, - flow2 = blockchainProviderTypesStore.get(), + flow2 = blockchainProvidersTypesManager.get(), // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) // don't use Lazily because some features (WC) require initialized factory on app started .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } - - private fun CoroutineScope.updateBlockchainProviderTypes() { - launch { - val response = blockchainProvidersResponseLoader.load() - - if (response == null) { - Timber.e("Error loading BlockchainProviderTypes") - return@launch - } - - Timber.i("Update BlockchainProviderTypes") - - blockchainProviderTypesStore.store( - value = BlockchainProviderTypesConverter.convert(response), - ) - } - } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 4b8a887b99..1c9cc512e4 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles +import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import timber.log.Timber import javax.inject.Inject diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt index c504dbaae7..e1a287d634 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -2,8 +2,8 @@ package com.tangem.blockchainsdk.converters import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.network.providers.ProviderType -import com.tangem.blockchainsdk.BlockchainProviderTypes import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import com.tangem.blockchainsdk.utils.createPrivateProviderType import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.config.providers.models.ProviderModel diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index e0ac497835..ec39268431 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -7,12 +7,14 @@ import com.tangem.blockchainsdk.WalletManagerFactoryCreator import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles -import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader -import com.tangem.blockchainsdk.store.DefaultRuntimeStore +import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager +import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager +import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.libs.blockchain_sdk.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -27,20 +29,32 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainSDKFactory( - blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + blockchainProvidersTypesManager: BlockchainProvidersTypesManager, environmentConfigStorage: EnvironmentConfigStorage, walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ): BlockchainSDKFactory { return DefaultBlockchainSDKFactory( - blockchainProvidersResponseLoader = blockchainProvidersResponseLoader, + blockchainProvidersTypesManager = blockchainProvidersTypesManager, environmentConfigStorage = environmentConfigStorage, - blockchainProviderTypesStore = DefaultRuntimeStore(defaultValue = emptyMap()), walletManagerFactoryCreator = walletManagerFactoryCreator, dispatchers = dispatchers, ) } + @Provides + @Singleton + fun provideBlockchainProvidersTypesManager( + devBlockchainProvidersTypesManager: DevBlockchainProvidersTypesManager, + prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager, + ): BlockchainProvidersTypesManager { + return if (BuildConfig.DEBUG) { + devBlockchainProvidersTypesManager + } else { + prodBlockchainProvidersTypesManager + } + } + @Provides @Singleton fun provideWalletManagerFactoryCreator( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProviderTypesStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProviderTypesStore.kt new file mode 100644 index 0000000000..f9f0706161 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProviderTypesStore.kt @@ -0,0 +1,18 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.datasource.local.datastore.RuntimeStateStore +import javax.inject.Inject +import javax.inject.Singleton + +internal typealias BlockchainProviderTypes = Map> + +/** + * Blockchain provider types store + * +[REDACTED_AUTHOR] + */ +@Singleton +class BlockchainProviderTypesStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore(emptyMap()) \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt new file mode 100644 index 0000000000..4336fc4db4 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -0,0 +1,52 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Loader of [BlockchainProvidersResponse] + * + * @property tangemTechApi tangem tech api + * @property blockchainProvidersStorage blockchain providers storage + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class BlockchainProvidersResponseLoader @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val blockchainProvidersStorage: BlockchainProvidersStorage, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** Load [BlockchainProvidersResponse] */ + suspend fun load(): BlockchainProvidersResponse? { + val localResponse = loadLocal().ifEmpty { return null } + + return loadRemote().fold( + onSuccess = { remoteResponse -> + BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteResponse, + ) + }, + onFailure = { + Timber.e(it, "Failed to load blockchain provider types from backend") + localResponse + }, + ) + } + + private suspend fun loadLocal(): BlockchainProvidersResponse = blockchainProvidersStorage.getConfigSync() + + private suspend fun loadRemote() = runCatching( + dispatcher = dispatchers.io, + block = tangemTechApi::getBlockchainProviders, + ) +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt similarity index 57% rename from libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index 049829a287..8afaacc12d 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -1,66 +1,33 @@ -package com.tangem.blockchainsdk.loader +package com.tangem.blockchainsdk.providers import androidx.core.util.PatternsCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.blockchainsdk.utils.createPrivateProviderType -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.models.ProviderModel -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton /** - * Loader of [BlockchainProvidersResponse] - * - * @property tangemTechServiceApi tangem tech api - * @property blockchainProvidersStorage blockchain providers storage - * @property dispatchers dispatchers + * Merger of [BlockchainProvidersResponse] * [REDACTED_AUTHOR] */ -@Singleton -internal class BlockchainProvidersResponseLoader @Inject constructor( - private val tangemTechServiceApi: TangemTechApi, - private val blockchainProvidersStorage: BlockchainProvidersStorage, - private val dispatchers: CoroutineDispatcherProvider, -) { +internal object BlockchainProvidersResponseMerger { private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) - /** Load [BlockchainProvidersResponse] */ - suspend fun load(): BlockchainProvidersResponse? { - val localResponse = loadLocal().ifEmpty { return null } + private val forbiddenSchemes = listOf("wss://") - return runCatching(dispatcher = dispatchers.io, block = ::loadRemote) - .fold( - onSuccess = { remoteResponse -> mergeResponses(local = localResponse, remote = remoteResponse) }, - onFailure = { - Timber.e(it, "Failed to load blockchain provider types from backend") - localResponse - }, - ) - } - - private suspend fun loadLocal(): BlockchainProvidersResponse = blockchainProvidersStorage.getConfigSync() - - private suspend fun loadRemote() = tangemTechServiceApi.getBlockchainProviders() - - /** Merge blockchains with non-empty providers [remote] from remote with blockchains from local [local] */ - private fun mergeResponses( - local: BlockchainProvidersResponse, - remote: BlockchainProvidersResponse, - ): BlockchainProvidersResponse { - /* - * Example: - * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) - * val local = mapOf("a" to 11, "e" to 4, "f" to 5) - * - * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } - */ + /** + * Merge blockchains with non-empty providers from [remote] with blockchains from [local] + * + * Example: + * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) + * val local = mapOf("a" to 11, "e" to 4, "f" to 5) + * + * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } + */ + fun merge(local: BlockchainProvidersResponse, remote: BlockchainProvidersResponse): BlockchainProvidersResponse { val remoteWithoutInvalidProviders = remote .mapValues { it.value @@ -136,8 +103,4 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( this } } - - private companion object { - val forbiddenSchemes = listOf("wss://") - } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..af43d092c8 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt @@ -0,0 +1,11 @@ +package com.tangem.blockchainsdk.providers + +import kotlinx.coroutines.flow.StateFlow + +/** Blockchain providers types manager */ +interface BlockchainProvidersTypesManager { + + fun get(): StateFlow + + suspend fun update() +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..479c90f1d2 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt @@ -0,0 +1,45 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.ProviderType +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.properties.Delegates.notNull + +/** + * Implementation of [BlockchainProvidersTypesManager] in DEV environment + * + * @property prodBlockchainProvidersTypesManager prod manager + * @property blockchainProviderTypesStore blockchain provider types store + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class DevBlockchainProvidersTypesManager @Inject constructor( + private val prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager, + private val blockchainProviderTypesStore: BlockchainProviderTypesStore, +) : BlockchainProvidersTypesManager by prodBlockchainProvidersTypesManager, + MutableBlockchainProvidersTypesManager { + + private var primaryResponse: BlockchainProviderTypes by notNull() + + override suspend fun update() { + prodBlockchainProvidersTypesManager.update() + + primaryResponse = blockchainProviderTypesStore.get().value + } + + override suspend fun update(blockchain: Blockchain, providers: List) { + val currentTypes = blockchainProviderTypesStore.get().value + + val updatedTypes = currentTypes + (blockchain to providers) + + blockchainProviderTypesStore.store(value = updatedTypes) + } + + override suspend fun isMatchWithMerged(): Boolean { + val current = blockchainProviderTypesStore.get().value + + return current == primaryResponse + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..148a2dfc23 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt @@ -0,0 +1,18 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.ProviderType + +/** + * Mutable blockchain providers types manager + * +[REDACTED_AUTHOR] + */ +interface MutableBlockchainProvidersTypesManager : BlockchainProvidersTypesManager { + + /** Update [providers] for [blockchain] */ + suspend fun update(blockchain: Blockchain, providers: List) + + /** Checks that current [BlockchainProviderTypes] was changed */ + suspend fun isMatchWithMerged(): Boolean +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..3a62906d7b --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt @@ -0,0 +1,39 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of [BlockchainProvidersTypesManager] in PROD environment + * + * @property blockchainProvidersResponseLoader blockchain providers response loader + * @property blockchainProviderTypesStore blockchain provider types store + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class ProdBlockchainProvidersTypesManager @Inject constructor( + private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val blockchainProviderTypesStore: BlockchainProviderTypesStore, +) : BlockchainProvidersTypesManager { + + override fun get(): StateFlow = blockchainProviderTypesStore.get() + + override suspend fun update() { + val response = blockchainProvidersResponseLoader.load() + + if (response == null) { + Timber.e("Error loading BlockchainProviderTypes") + return + } + + Timber.i("Update BlockchainProviderTypes") + + blockchainProviderTypesStore.store( + value = BlockchainProviderTypesConverter.convert(response), + ) + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt deleted file mode 100644 index 4278918e98..0000000000 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.blockchainsdk.store - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -/** - * Default implementation of RuntimeStore - * - * @param defaultValue default value - */ -internal class DefaultRuntimeStore(defaultValue: T) : RuntimeStore { - - private val flow = MutableStateFlow(value = defaultValue) - - override fun get(): StateFlow = flow - - override suspend fun store(value: T) { - flow.value = value - } -} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt deleted file mode 100644 index 2969e42576..0000000000 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.blockchainsdk.store - -import kotlinx.coroutines.flow.StateFlow - -/** - * Runtime store - * -[REDACTED_AUTHOR] - */ -internal interface RuntimeStore { - - /** Get flow of elements [T] */ - fun get(): StateFlow - - /** Store [value] */ - suspend fun store(value: T) -} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 952be8affd..a0d9c16d6f 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -106,6 +106,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "nexa" -> Blockchain.Nexa // FIXME "nexa/test" -> Blockchain.NexaTestnet // FIXME "radiant" -> Blockchain.Radiant + "fact0rn" -> Blockchain.Fact0rn "moonriver" -> Blockchain.Moonriver "moonriver/test" -> Blockchain.MoonriverTestnet "mantle" -> Blockchain.Mantle @@ -142,9 +143,15 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "casper-network/test" -> Blockchain.CasperTestnet "chiliz" -> Blockchain.Chiliz "chiliz/test" -> Blockchain.ChilizTestnet + "vanar-chain" -> Blockchain.VanarChain + "vanar-chain/test" -> Blockchain.VanarChainTestnet "xodex" -> Blockchain.Xodex "canxium" -> Blockchain.Canxium "clore-ai" -> Blockchain.Clore + "dione" -> Blockchain.OdysseyChain + "dione/test" -> Blockchain.OdysseyChainTestnet + "bitrock" -> Blockchain.Bitrock + "bitrock/test" -> Blockchain.BitrockTestnet else -> null } } @@ -250,6 +257,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.Nexa -> "nexa" // FIXME Blockchain.NexaTestnet -> "nexa/test" // FIXME Blockchain.Radiant -> "radiant" + Blockchain.Fact0rn -> "fact0rn" Blockchain.Moonriver -> "moonriver" Blockchain.MoonriverTestnet -> "moonriver/test" Blockchain.Mantle -> "mantle" @@ -284,9 +292,15 @@ fun Blockchain.toNetworkId(): String { Blockchain.CoreTestnet -> "core/test" Blockchain.Chiliz -> "chiliz" Blockchain.ChilizTestnet -> "chiliz/test" + Blockchain.VanarChain -> "vanar-chain" + Blockchain.VanarChainTestnet -> "vanar-chain/test" Blockchain.Xodex -> "xodex" Blockchain.Canxium -> "canxium" Blockchain.Clore -> "clore-ai" + Blockchain.OdysseyChain -> "dione" + Blockchain.OdysseyChainTestnet -> "dione/test" + Blockchain.Bitrock -> "bitrock" + Blockchain.BitrockTestnet -> "bitrock/test" } } @@ -358,6 +372,7 @@ fun Blockchain.toCoinId(): String { Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum" Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME Blockchain.Radiant -> "radiant" + Blockchain.Fact0rn -> "fact0rn" Blockchain.Moonriver, Blockchain.MoonriverTestnet -> "moonriver" Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle" Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" @@ -377,9 +392,12 @@ fun Blockchain.toCoinId(): String { Blockchain.Casper, Blockchain.CasperTestnet -> "casper-network" Blockchain.Core, Blockchain.CoreTestnet -> "coredaoorg" Blockchain.Chiliz, Blockchain.ChilizTestnet -> "chiliz" + Blockchain.VanarChain, Blockchain.VanarChainTestnet -> "vanar-chain" Blockchain.Xodex -> "xodex" Blockchain.Canxium -> "canxium" Blockchain.Clore -> "clore-ai" + Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> "dione" + Blockchain.Bitrock, Blockchain.BitrockTestnet -> "bitrock" } } diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt deleted file mode 100644 index f0233cde5a..0000000000 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt +++ /dev/null @@ -1,245 +0,0 @@ -package com.tangem.blockchainsdk.loader - -import com.google.common.truth.Truth -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchainsdk.BlockchainProvidersResponse -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage -import com.tangem.datasource.local.config.providers.models.ProviderModel -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -/** -[REDACTED_AUTHOR] - */ -internal class BlockchainProvidersResponseLoaderTest { - - private val tangemTechServiceApi = mockk() - private val blockchainProvidersStorage = mockk() - - private val loader = BlockchainProvidersResponseLoader( - tangemTechServiceApi = tangemTechServiceApi, - blockchainProvidersStorage = blockchainProvidersStorage, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - private val firebaseCrashlytics = mockk() - - @Before - fun setup() { - mockkStatic(FirebaseCrashlytics::class) - every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics - } - - @Test - fun test_load_if_local_config_is_empty() = runTest { - coEvery { blockchainProvidersStorage.getConfigSync() } returns emptyMap() - - val actual = loader.load() - - coVerifyOrder { blockchainProvidersStorage.getConfigSync() } - coVerify(inverse = true) { tangemTechServiceApi.getBlockchainProviders() } - - Truth.assertThat(actual).isEqualTo(null) - } - - @Test - fun test_load_if_remote_config_loading_is_loaded_failure() = runTest { - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } throws IllegalStateException("Test exception") - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(localProviders) - } - - @Test - fun test_load_if_remote_config_loading_is_loaded_successful() = runTest { - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_is_the_same_as_local() = runTest { - val remoteProviders = localProviders - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_has_empty_providers() = runTest { - val ethProvider = "ethereum" to emptyList() - val remoteProvidersWithEth = remoteProviders + ethProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_local_config_has_empty_providers() = runTest { - val localProvidersWithEmptyApt = localProviders.mapValues { if (it.key == "aptos") emptyList() else it.value } - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEmptyApt - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - val expected = localProvidersWithEmptyApt + remoteProviders - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun test_load_if_remote_config_doesnt_contain_local_providers() = runTest { - val remoteProvidersWithoutLocal: BlockchainProvidersResponse = remoteProviders - localProviders.keys - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithoutLocal - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_contains_unsupported_types() = runTest { - val ethProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Private(name = "nownodes")) - val remoteProvidersWithEth = remoteProviders + ethProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - val expected = remoteProviders + ("ethereum" to listOf(ProviderModel.Private(name = "nownodes"))) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun test_load_if_remote_config_contains_invalid_public_providers() = runTest { - val localEthProvider = "ethereum" to listOf(ProviderModel.Private(name = "nownodes")) - val localProvidersWithEth = localProviders + localEthProvider - - val remoteEthProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138")) - val remoteProvidersWithEth = remoteProviders + remoteEthProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEth - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - val expected = remoteProviders + localEthProvider - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun test_load_if_configs_contain_public_providers_without_slash_in_the_end() = runTest { - val localPublicProviderUrl = "https://qwe.com" - val localEthProvider = "ethereum" to listOf(ProviderModel.Public(url = localPublicProviderUrl)) - val localProvidersWithEth = localProviders + localEthProvider - - val remotePublicProviderUrl = "https://rty.com" - val remoteDogeProvider = "dogecoin" to listOf(ProviderModel.Public(url = remotePublicProviderUrl)) - val remoteProvidersWithDoge = remoteProviders + remoteDogeProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEth - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithDoge - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - val expected = remoteProviders + - localEthProvider.copy(second = listOf(ProviderModel.Public(url = "$localPublicProviderUrl/"))) + - remoteDogeProvider.copy(second = listOf(ProviderModel.Public(url = "$remotePublicProviderUrl/"))) - - Truth.assertThat(actual).isEqualTo(expected) - } - - private fun everyCrashlyticsRecording() = every { firebaseCrashlytics.recordException(any()) } - - private companion object { - val localProviders: BlockchainProvidersResponse = mapOf( - "aptos" to listOf(ProviderModel.Private(name = "nownodes")), - "algorand" to listOf( - ProviderModel.Private(name = "nownodes"), - ProviderModel.Public(url = "https://public_alg.com/"), - ), - ) - - val remoteProviders: BlockchainProvidersResponse = localProviders + mapOf( - "bitcoin" to listOf(ProviderModel.Private(name = "blockchair")), - ) - } -} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt new file mode 100644 index 0000000000..54d99add4a --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt @@ -0,0 +1,88 @@ +package com.tangem.blockchainsdk.providers + +import com.google.common.truth.Truth +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.localResponse +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.remoteResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage +import com.tangem.datasource.local.config.providers.models.ProviderModel +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class BlockchainProvidersResponseLoaderTest { + + private val tangemTechApi = mockk() + private val blockchainProvidersStorage = mockk() + + private val loader = BlockchainProvidersResponseLoader( + tangemTechApi = tangemTechApi, + blockchainProvidersStorage = blockchainProvidersStorage, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Before + fun setup() { + mockkStatic(FirebaseCrashlytics::class) + val firebaseCrashlytics = mockk() + every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics + every { firebaseCrashlytics.recordException(any()) } just Runs + } + + @Test + fun test_if_local_config_is_empty() = runTest { + coEvery { blockchainProvidersStorage.getConfigSync() } returns emptyMap() + + val expected = null + + val actual = loader.load() + + coVerifyOrder { blockchainProvidersStorage.getConfigSync() } + coVerify(inverse = true) { tangemTechApi.getBlockchainProviders() } + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_loading_is_failed() = runTest { + coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse + coEvery { tangemTechApi.getBlockchainProviders() } throws IllegalStateException("Test exception") + + val expected = localResponse + + val actual = loader.load() + + coVerifyOrder { + blockchainProvidersStorage.getConfigSync() + tangemTechApi.getBlockchainProviders() + } + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_is_loaded_successfully() = runTest { + val eth = "ethereum" to listOf(ProviderModel.Private(name = "nownodes")) + + coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse + eth + coEvery { tangemTechApi.getBlockchainProviders() } returns remoteResponse + + // Because configs are merged in BlockchainProvidersResponseMerger + val expected = remoteResponse + eth + + val actual = loader.load() + + coVerifyOrder { + blockchainProvidersStorage.getConfigSync() + tangemTechApi.getBlockchainProviders() + } + + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt new file mode 100644 index 0000000000..53004c77b9 --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt @@ -0,0 +1,205 @@ +package com.tangem.blockchainsdk.providers + +import com.google.common.truth.Truth +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.local.config.providers.models.ProviderModel +import io.mockk.* +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class BlockchainProvidersResponseMergerTest { + + @Before + fun setup() { + mockkStatic(FirebaseCrashlytics::class) + val firebaseCrashlytics = mockk() + every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics + every { firebaseCrashlytics.recordException(any()) } just Runs + } + + @Test + fun test_if_both_configs_are_empty() { + val expected = emptyMap>() + + val actual = BlockchainProvidersResponseMerger.merge( + local = emptyMap(), + remote = emptyMap(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_local_config_is_empty() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = emptyMap(), + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_is_empty() { + val expected = localResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = emptyMap(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_both_configs_are_not_empty() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_configs_are_equal() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = remoteResponse, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_local_config_has_blockchain_with_empty_providers() { + val eth = "ethereum" to emptyList() + val localResponseWithEth = localResponse + eth + + val expected = localResponseWithEth + remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponseWithEth, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_has_blockchain_with_empty_providers() { + val expected = remoteResponse + + val eth = "ethereum" to emptyList() + val remoteWithEth = remoteResponse + eth + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_doesnt_contain_local_providers() { + val remoteWithoutLocal = remoteResponse - localResponse.keys + + /** + * The expected result does not contain local blockchains, since they can be disabled remotely. + * For the opposite case, see [test_if_both_configs_are_not_empty]. + */ + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithoutLocal, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_contains_unsupported_providers() { + val nowNodesProvider = ProviderModel.Private(name = "nownodes") + val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, nowNodesProvider) + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + ("ethereum" to listOf(nowNodesProvider)) + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_contains_invalid_public_providers() { + val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138")) + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_configs_contain_public_providers_without_slash_in_the_end() { + val eth = "ethereum" to listOf(ProviderModel.Public("https://qwe.com")) + val kaspa = "kaspa" to listOf(ProviderModel.Public("https://qwe.com")) + + val localWithKaspa = localResponse + kaspa + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + eth.addSlash() + kaspa.addSlash() + + val actual = BlockchainProvidersResponseMerger.merge( + local = localWithKaspa, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + companion object { + + val localResponse: BlockchainProvidersResponse = mapOf( + "aptos" to listOf(ProviderModel.Private(name = "nownodes")), + "algorand" to listOf( + ProviderModel.Private(name = "nownodes"), + ProviderModel.Public(url = "https://public_alg.com/"), + ), + ) + + // local + bitcoin + val remoteResponse: BlockchainProvidersResponse = mapOf( + "aptos" to listOf(ProviderModel.Private(name = "nownodes")), + "algorand" to listOf( + ProviderModel.Private(name = "nownodes"), + ProviderModel.Public(url = "https://public_alg.com/"), + ), + "bitcoin" to listOf(ProviderModel.Private(name = "blockchair")), + ) + + private fun Pair>.addSlash(): Pair> { + return first to second.map { it.copy(url = "${it.url}/") } + } + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 5e1a12c171..ab076272bd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -5,8 +5,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter import com.tangem.lib.crypto.models.XrpTaggedAddress +import java.math.BigDecimal /** * !!!IMPORTANT!!! @@ -116,6 +118,8 @@ object BlockchainUtils { return l2BlockchainsList.contains(blockchain) } + fun getTezosThreshold(): BigDecimal = Blockchain.Tezos.minimalAmount() + /** * Blockchains not affecting total balance counting on errors */ diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 91756f3226..514702adfa 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) + implementation(projects.domain.visa.models) implementation(projects.core.res) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index a2c6cc8c4f..d86b31b3d0 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -15,6 +15,9 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaCardActivationResponse import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter @@ -141,4 +144,14 @@ interface TangemSdkManager { fun clearProductType() // endregion + + // region Visa-specific + + suspend fun activateVisaCard( + accessCode: String, + challengeToSign: VisaAuthChallenge.Card?, + activationInput: VisaActivationInput, + ): CompletionResult + + // endregion } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1db213040e..5e944733b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -232,6 +232,7 @@ include(":domain:transaction") include(":domain:transaction:models") include(":domain:analytics") include(":domain:visa") +include(":domain:visa:models") include(":domain:onboarding") include(":domain:feedback") include(":domain:qr-scanning") @@ -245,6 +246,7 @@ include(":domain:manage-tokens") include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") +include(":domain:promo") // endregion Domain modules // region Data modules @@ -269,4 +271,5 @@ include(":data:wallet-connect") include(":data:markets") include(":data:manage-tokens") include(":data:onramp") -// endregion Data modules \ No newline at end of file +// endregion Data modules +include(":domain:promo:models") \ No newline at end of file diff --git a/version.properties b/version.properties index 3e279d9f6c..b87bff9bef 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.19.0 \ No newline at end of file +versionName=5.20.0 \ No newline at end of file