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..3886d9f58b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,6 +75,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 +167,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 +228,11 @@ 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) /** Testing libraries */ testImplementation(deps.test.coroutine) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fbb777f36c..4e84fb633b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,11 +10,6 @@ - - - diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index f1aa2f2cbf..1f1c301f4e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit f1aa2f2cbf374ecba5da753d9f51c9df81749ef2 +Subproject commit 1f1c301f4e0286cf6ab4cd8ee9323c85de3dfaab 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/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 95b59844e2..80eac8dc24 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -10,6 +10,7 @@ import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager 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 @@ -36,7 +37,6 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles 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 +113,6 @@ interface ApplicationEntryPoint { fun getTransactionSignerFactory(): TransactionSignerFactory - fun getHomeFeatureToggles(): HomeFeatureToggles - fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase fun getOnrampFeatureToggles(): OnrampFeatureToggles @@ -128,4 +126,6 @@ interface ApplicationEntryPoint { fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore + + fun getClipboardManager(): ClipboardManager } \ 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/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2bc6faa40c..b5cd3eab19 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,14 @@ 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.UiDependencies 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.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 @@ -85,8 +92,9 @@ 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 +217,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 @@ -255,8 +269,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac installActivityDependencies() observeAppThemeModeUpdates() - setContentView(R.layout.activity_main) - installRouting() + if (routingFeatureToggles.isNavigationRefactoringEnabled) { + setRootContent() + } else { + setContentView(R.layout.activity_main) + installRouting() + installEventMessageEffect() + } + initContent() observeStateUpdates() @@ -271,6 +291,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,6 +354,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac is RoutingComponent.Child.LegacyIntent -> { startActivity(child.intent) } + is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child") } } } @@ -400,8 +458,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) } @@ -481,22 +542,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 +592,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 +609,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) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 05fd529eaa..83bba376a4 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -20,6 +20,7 @@ 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.ui.clipboard.ClipboardManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager @@ -58,7 +59,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 @@ -179,9 +179,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 +199,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val appLogsStore: AppLogsStore get() = entryPoint.getAppLogsStore() + + private val clipboardManager: ClipboardManager + get() = entryPoint.getClipboardManager() // endregion override fun onCreate() { @@ -292,7 +292,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - homeFeatureToggles = homeFeatureToggles, getUserCountryUseCase = getUserCountryUseCase, onrampFeatureToggles = onrampFeatureToggles, environmentConfigStorage = environmentConfigStorage, @@ -300,6 +299,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, + clipboardManager = clipboardManager, ), ), ) 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/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/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/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/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/domain/PromoDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt new file mode 100644 index 0000000000..ccc91c699c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.ShouldShowRingPromoUseCase +import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase +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) + } +} \ 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/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 59dea35070..164992b425 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 @@ -35,7 +36,10 @@ 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.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask @@ -122,7 +126,7 @@ class DefaultTangemSdkManager( messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { - val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) + val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, @@ -154,9 +158,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 +198,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 +247,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 +255,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 +285,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 +293,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 +301,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 +312,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 +323,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 +376,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) { 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..0d454c11e8 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,6 +12,7 @@ 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 @@ -150,7 +151,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) { 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/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/main/MainScreenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt index 9bbeded200..c2c010454e 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt @@ -35,7 +35,7 @@ internal class MainScreenStateHolder(private val intents: MainIntents) { stateFlowInternal.update { state -> state.copy( modalNotification = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = intents::onDismissBottomSheet, content = notificationsFactory.createBalancesAreHiddenModalNotification(), ), @@ -45,7 +45,7 @@ internal class MainScreenStateHolder(private val intents: MainIntents) { fun updateWithoutModalNotification() { stateFlowInternal.update { state -> - state.copy(modalNotification = state.modalNotification?.copy(isShow = false)) + state.copy(modalNotification = state.modalNotification?.copy(isShown = false)) } } 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..375b285fb1 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 @@ -18,7 +18,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 @@ -41,7 +40,6 @@ internal class MainViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val apiConfigsManager: ApiConfigsManager, - homeFeatureToggles: HomeFeatureToggles, private val fetchUserCountryUseCase: FetchUserCountryUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -63,11 +61,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") } } @@ -129,7 +125,7 @@ internal class MainViewModel @Inject constructor( it.isBalanceHidingNotificationEnabled && it.isBalanceHidden } .onEach { - if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) { + if (state.value.modalNotification?.isShown != true && !it.isUpdateFromToast) { listenToFlipsUseCase.changeUpdateEnabled(false) stateHolder.updateWithHiddenBalancesNotification() router.push(AppRoute.ModalNotification) 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 index ef72720cad..6e4e1517e4 100644 --- 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 @@ -50,7 +50,7 @@ internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment } LaunchedEffect(notification) { - if (!notification.isShow) { + if (!notification.isShown) { dismiss() } } 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..8859c88746 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().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/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..392f1aacd9 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().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/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt index 5ee9a4ce7f..5488669d34 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,6 +141,8 @@ 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 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..4a16217522 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,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Xodex -> null Canxium -> null Clore -> null + VanarChain -> null + VanarChainTestnet -> 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..ef18e4fe77 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 @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter 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 +36,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 +69,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 +76,5 @@ data class DaggerGraphState( val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, val appPreferencesStore: AppPreferencesStore? = null, + val clipboardManager: ClipboardManager? = 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..960a2bd499 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 @@ -33,15 +33,17 @@ import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCar 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 +56,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 +64,174 @@ 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.ModalNotification, + 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 @@ -248,6 +413,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..a1afc8ea37 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,7 +1,28 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.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) } \ No newline at end of file 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/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/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/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json index ae77ec7b7c..a2862f948e 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,13 @@ { "name": "NEXA/test", "version": "undefined" + }, + { + "name": "fact0rn", + "version": "undefined" + }, + { + "name": "vanar-chain", + "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..60580e40b7 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,9 @@ { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", "version": "undefined" + }, + { + "name": "NAVIGATION_REFACTORING", + "version": "undefined" } ] 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..a31ec2e6d8 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,7 @@ sealed class ApiConfig { Express, TangemTech, StakeKit, + TangemVisaAuth, } private fun initializeId(): ID { @@ -31,6 +32,7 @@ sealed class ApiConfig { is Express -> ID.Express is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit + is TangemVisaAuth -> ID.TangemVisaAuth } } 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..102bf875d4 --- /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.Provider + +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/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..cd538c51eb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.api.visa + +interface TangemVisaApi { + // TODO +} \ 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/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..7daabbc3e8 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,7 +1,7 @@ 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 @@ -39,4 +39,8 @@ internal object ApiConfigsModule { @IntoSet fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig = TangemTech(appVersionProvider, authProvider) + + @Provides + @IntoSet + fun provideTangemVisaConfig(): ApiConfig = TangemVisaAuth() } \ No newline at end of file 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..84cc46d45c 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,7 @@ 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.TangemVisaAuthApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* @@ -169,6 +170,21 @@ internal object NetworkModule { ) } + @Provides + @Singleton + fun provideTangemVisaApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + apiConfigsManager: ApiConfigsManager, + ): TangemVisaAuthApi { + return createApi( + id = ApiConfig.ID.TangemVisaAuth, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + ) + } + @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 createExpressModel() is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() + is TangemVisaAuth -> createVisaAuthModel() } } @@ -172,6 +173,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } + private fun createVisaAuthModel(): Model { + return Model( + id = ApiConfig.ID.TangemVisaAuth, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.STAGE, + baseUrl = "https://api-s.tangem.org/", + ), + ) + } + private fun String.checkHeaderValueOrEmpty(): String { for (i in this.indices) { val c = this[i] 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/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/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index e741b087fe..ac57417425 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 @@ -16,7 +16,6 @@ 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/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/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..f1cd043fd6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -0,0 +1,106 @@ +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, + ) + + StoriesProgressBar( + steps = storyState.steps, + currentStep = storyState.currentIndex.intValue, + stepDuration = storyState.currentStory.duration, + paused = isPaused, + onStepFinish = storyState::nextStory, + ) + + currentStoryContent(storyState.currentStory, isPaused) + } +} + +// 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, +) : 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..f2c504316e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt @@ -0,0 +1,127 @@ +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 + .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..68cc624895 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/model/StoriesContentConfig.kt @@ -0,0 +1,18 @@ +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 +} + +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..d3d5ab0171 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,7 @@ 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 else -> R.drawable.ic_alert_24 } } @@ -166,6 +167,7 @@ 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 else -> R.drawable.ic_alert_24 } } @@ -251,6 +253,7 @@ 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 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..0828f90e8d 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,14 @@ package com.tangem.core.ui.message -import androidx.compose.material3.SnackbarDuration +import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable 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,16 +22,40 @@ 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. @@ -44,4 +71,170 @@ data class ContentMessage(val content: Content) : EventMessage { @Composable operator fun invoke(onDismiss: () -> Unit) } +} + +/** + * Shows an alert dialog. + * + * @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 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 { + + companion object { + + /** + * 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..7a4451f689 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,26 +1,49 @@ 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 SnackbarMessage -> { + onShowSnackbar(message, context) + } is ContentMessage -> { contentMessage = message } - is SnackbarMessage -> { - showSnackbar(snackbarHostState, message, context) + is DialogMessage -> { + dialogMessage = message + } + is BottomSheetMessage -> { + bottomSheetMessage = message } } } @@ -28,16 +51,110 @@ fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: S 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, + ) } private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) { 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_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/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/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..9ca9d26d6a 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,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra Blockchain.Canxium, Blockchain.Chiliz, Blockchain.ChilizTestnet, + Blockchain.VanarChain, + Blockchain.VanarChainTestnet, -> 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..4714406f4d 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,25 @@ 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.utils.get +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 +53,42 @@ 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) + } + 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/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/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/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a8587868f9..8299b95fb4 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,10 @@ package com.tangem.data.visa.di +import com.tangem.data.visa.DefaultVisaAuthRepository import com.tangem.data.visa.DummyVisaRepository +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 +24,13 @@ internal object VisaDataModule { ): VisaRepository { return implementedVisaRepository.getOrNull() ?: DummyVisaRepository() } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface VisaDataBindsModule { + + @Binds + @Singleton + fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository } \ 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..904ee2d1d8 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 @@ -66,7 +67,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/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 1311a3016e..4d87eecc86 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 @@ -27,6 +28,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..cd511d679b 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 @@ -32,6 +33,6 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_subject_pre_activated_wallet is FeedbackEmailType.CurrencyDescriptionError -> R.string.feedback_token_description_error } - .let(resources::getString) + .let(resources::getStringSafe) } } \ No newline at end of file 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/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 440e818d8f..7c2147cd08 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -22,6 +22,7 @@ import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.util.hasDerivation +import com.tangem.domain.demo.BuildConfig import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -369,7 +370,7 @@ class DefaultWalletManagersFacade( blockchain = blockchain, derivationPath = derivationPath, ) - if (walletManager == null) { + if (BuildConfig.DEBUG || walletManager == null) { walletManager = walletManagerFactory.createWalletManager( scanResponse = userWallet.scanResponse, blockchain = blockchain, 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/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt similarity index 60% rename from domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt rename to domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index b68a740e09..6350df9b18 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -1,9 +1,17 @@ -package com.tangem.domain.settings.repositories +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 PromoSettingsRepository { +interface PromoRepository { + + suspend fun getChangellyPromoBanner(): PromoBanner? + + suspend fun getOkxPromoBanner(): PromoBanner? + + suspend fun getRingPromoBanner(): PromoBanner? + fun isReadyToShowWalletSwapPromo(): Flow fun isReadyToShowTokenSwapPromo(): Flow diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt similarity index 73% rename from domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt rename to domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt index 67d9174e78..6dfa5d7b2b 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowRingPromoUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt @@ -1,10 +1,9 @@ -package com.tangem.domain.settings +package com.tangem.domain.promo -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) { +class ShouldShowRingPromoUseCase(private val promoSettingsRepository: PromoRepository) { operator fun invoke(userWalletId: UserWalletId): Flow { return promoSettingsRepository.isReadyToShowRingPromo(userWalletId) diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt similarity index 66% rename from domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt rename to domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt index 8ede57bcf9..69908277b5 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoTokenUseCase.kt @@ -1,9 +1,8 @@ -package com.tangem.domain.settings +package com.tangem.domain.promo -import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoSettingsRepository) { +class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoRepository) { operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowTokenSwapPromo() diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt similarity index 66% rename from domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt rename to domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt index 233277f71f..af234c43b2 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowSwapPromoWalletUseCase.kt @@ -1,9 +1,8 @@ -package com.tangem.domain.settings +package com.tangem.domain.promo -import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { +class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoRepository) { operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletSwapPromo() 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..d5f66c4889 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -20,4 +20,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/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt new file mode 100644 index 0000000000..536b7207dc --- /dev/null +++ b/domain/visa/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/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt new file mode 100644 index 0000000000..67437218f5 --- /dev/null +++ b/domain/visa/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/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt new file mode 100644 index 0000000000..a16916a887 --- /dev/null +++ b/domain/visa/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/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt new file mode 100644 index 0000000000..83b0a5b091 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@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/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..f400b2ee7d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,9 +21,17 @@ 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 @@ -42,7 +50,14 @@ platform :android do desc "Build external and release APKs" lane :build do |options| - gradle(task: 'clean') + gradle(task: 'clean') # TODO: remove after fully moving from old CI + + launched_from_github_action = options[:launched_from_github_action] || false # TODO: remove after fully moving from old CI + if launched_from_github_action + FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") + end + gradle( task: "bundle", build_type: "Release", @@ -79,19 +94,29 @@ platform :android do desc "Publish internal and external builds to Firebase App Distribution" lane :publishToFirebase do |options| + FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + puts File.read("../gradle.properties") + gradle( task: "clean assemble", build_type: "Internal", properties: { - 'versionCode' => ENV['versionCode'], - 'versionName' => ENV['versionName'], + 'versionCode' => ENV['version_code'], + 'versionName' => ENV['version_name'], } ) - firebase_app_distribution( + 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..450ed613ad 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 @@ -13,10 +12,7 @@ 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.extensions.* import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.ScanCardProcessor @@ -107,7 +103,7 @@ internal class UserWalletSaver @Inject constructor( messageSender.send( message = ContentMessage { onDismiss -> SimpleOkDialog( - message = stringResource(id = R.string.user_wallet_list_error_wallet_already_saved), + message = stringResourceSafe(id = R.string.user_wallet_list_error_wallet_already_saved), onDismissDialog = onDismiss, ) }, 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..55dbe5ca34 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 @@ -16,7 +16,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 @@ -31,6 +30,7 @@ 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,7 +153,7 @@ 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, 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..7ae0b35b09 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,6 +1,5 @@ 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 @@ -10,6 +9,7 @@ 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.extensions.stringResourceSafe import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.card.HasMissedDerivationsUseCase @@ -231,7 +231,7 @@ internal class CustomTokenFormModel @Inject constructor( private fun showErrorDialog() { val dialog = ContentMessage { onDismiss -> SimpleOkDialog( - message = stringResource(R.string.common_unknown_error), + message = stringResourceSafe(R.string.common_unknown_error), onDismissDialog = onDismiss, ) } 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..fd105e3e8c 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, 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 index 659b007c29..076cca7f32 100644 --- 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 @@ -1,11 +1,11 @@ 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.core.ui.extensions.stringResourceSafe import com.tangem.features.managetokens.impl.R @Composable @@ -14,7 +14,7 @@ internal fun CurrencyUnsupportedDialog(title: TextReference, message: TextRefere title = title.resolveReference(), message = message.resolveReference(), confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), + title = stringResourceSafe(R.string.common_ok), onClick = onDismiss, ), onDismissDialog = onDismiss, 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 index 848d654e08..3e84202ce6 100644 --- 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 @@ -1,9 +1,9 @@ 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.stringResourceSafe import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.impl.R @@ -11,18 +11,18 @@ import com.tangem.features.managetokens.impl.R @Composable internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) { BasicDialog( - title = stringResource( + title = stringResourceSafe( R.string.token_details_unable_hide_alert_title, currency.name, ), - message = stringResource( + message = stringResourceSafe( R.string.token_details_unable_hide_alert_message, currency.name, currency.symbol, network.name, ), confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), + title = stringResourceSafe(R.string.common_ok), onClick = onDismiss, ), onDismissDialog = onDismiss, 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 index 46b9cc6a39..547791ec51 100644 --- 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 @@ -1,27 +1,27 @@ 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.stringResourceSafe 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( + title = stringResourceSafe( R.string.token_details_hide_alert_title, currency.name, ), - message = stringResource(R.string.token_details_hide_alert_message), + message = stringResourceSafe(R.string.token_details_hide_alert_message), confirmButton = DialogButtonUM( - title = stringResource(R.string.token_details_hide_alert_hide), + title = stringResourceSafe(R.string.token_details_hide_alert_hide), warning = true, onClick = onConfirm, ), dismissButton = DialogButtonUM( - title = stringResource(R.string.common_cancel), + title = stringResourceSafe(R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, 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..cad8517d8c 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 @@ -152,7 +152,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)) } } 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..9461484567 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 @@ -50,7 +50,7 @@ internal class AddToPortfolioBSContentUMFactory( alreadyAddedNetworks: Set?, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, + isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, content = if (selectedWallet != null && alreadyAddedNetworks != null) { AddToPortfolioBSContentUM( @@ -107,7 +107,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/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index ce9e2b7841..cce4273617 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 @@ -101,10 +101,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 +130,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..51596ceb54 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 @@ -68,7 +68,7 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { @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 +150,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 +163,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 index 70e52d864c..e03f24de02 100644 --- 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 @@ -1,12 +1,12 @@ 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.core.ui.extensions.stringResourceSafe import com.tangem.features.markets.impl.R @Composable @@ -19,7 +19,7 @@ internal fun WarningDialog( title = title.resolveReference(), message = message.resolveReference(), confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), + title = stringResourceSafe(R.string.common_ok), onClick = onDismiss, ), onDismissDialog = onDismiss, 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/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/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..5512fd5d07 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 @@ -1,6 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui import androidx.compose.animation.* +import com.tangem.core.ui.extensions.stringResourceSafe import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -14,7 +15,6 @@ 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.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -55,49 +55,52 @@ 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, ) } + + 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() 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..9d37d02677 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 @@ -5,9 +5,7 @@ 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.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -18,6 +16,7 @@ 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.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,48 +47,50 @@ 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, + ) } } 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/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..0a5bb21947 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 @@ -9,7 +9,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel import kotlinx.coroutines.flow.SharedFlow -class OnboardingVisaAccessCodeComponent( +internal class OnboardingVisaAccessCodeComponent( appComponentContext: AppComponentContext, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { 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..d75af43b85 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 @@ -10,7 +10,7 @@ import javax.inject.Inject @Stable @ComponentScoped -class OnboardingVisaAccessCodeModel @Inject constructor( +internal class OnboardingVisaAccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val paramsContainer: ParamsContainer, ) : Model() { 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..cf16e457eb --- /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.atLeast4CharError, + visualTransformation = if (state.accessCodeHidden) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + }, + caption = when { + state.codesNotMatchError && reEnterAccessCodeState -> + stringResource(R.string.onboarding_access_codes_doesnt_match) + state.atLeast4CharError && !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..68719daa53 --- /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 atLeast4CharError: 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/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..b9ee1576da 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 @@ -4,10 +4,10 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource 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.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.ui.components.Description @@ -29,7 +29,6 @@ fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modi .weight(1f) .fillMaxWidth(), ) { - item { OnboardingDescriptionBlock( modifier = Modifier @@ -59,7 +58,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, 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..8f9c0f7056 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 @@ -14,7 +14,6 @@ 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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -22,8 +21,9 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter 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 +49,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 +70,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,8 +108,8 @@ 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, ) } 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..e7f8a4d6dc 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, ) 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/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index f53b13e685..5a09dc2d37 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 @@ -14,6 +13,7 @@ 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.extensions.stringResourceSafe import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.* @@ -358,7 +358,7 @@ internal class OnrampMainComponentModel @Inject constructor( ) val dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) 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..df74ea2d77 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,6 +1,5 @@ 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 @@ -10,10 +9,7 @@ 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.extensions.* import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase import com.tangem.domain.onramp.model.error.OnrampError @@ -88,9 +84,9 @@ internal class OnrampRedirectModel @Inject constructor( ) val contentMessage = ContentMessage { onDismiss -> BasicDialog( - message = stringResource(id = R.string.common_unknown_error), + message = stringResourceSafe(id = R.string.common_unknown_error), confirmButton = DialogButtonUM( - title = stringResource(id = R.string.common_ok), + title = stringResourceSafe(id = R.string.common_ok), onClick = { params.onBack() onDismiss() 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..fa24e2dc0f 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 @@ -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/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..6d6e01358c 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 @@ -14,6 +13,7 @@ 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.extensions.stringResourceSafe import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -141,7 +141,7 @@ internal class OnrampOperationModel @Inject constructor( ) val dismissButton = DialogButtonUM( - title = stringResource(id = R.string.common_cancel), + title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss, ) 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/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/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/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..cc5dd3518a 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.openSettings() } } \ 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/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..1cf81e5c50 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,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( Chiliz, ChilizTestnet, Clore, + VanarChain, + VanarChainTestnet, -> Fee.Common(feeAmount) // endregion } @@ -1150,11 +1078,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 +1105,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 +1138,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = txFee, - transactionFee = txFeeResult?.getOrNull(), + transactionFee = txFeeResult.getOrNull(), includeFeeInAmount = includeFeeInAmount, + selectedFee = selectedFee, ) } } @@ -1225,6 +1158,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFee: TxFeeState, transactionFee: TransactionFee?, includeFeeInAmount: IncludeFeeInAmount, + selectedFee: FeeType, ): SwapState { return quoteDataModel.fold( ifRight = { quoteModel -> @@ -1238,12 +1172,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 +1263,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 +1284,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 +1306,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 +1402,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 +1417,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 +1431,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 +1538,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapDataModel = swapData, txFee = txFeeState, swapProvider = provider, + minAdaValue = null, ) } @@ -1600,20 +1561,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 +2175,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..bd52ee999a 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -60,7 +60,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 +70,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/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 8b21ec6919..50efc0c19f 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, @@ -106,35 +109,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/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index c25fc38225..05e17e542b 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,8 +16,8 @@ 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 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/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/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..58a9aabddf 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?, @@ -110,6 +119,7 @@ internal class StateBuilder( providerState = ProviderState.Empty(), shouldShowMaxAmount = false, priceImpact = PriceImpact.Empty(), + isInsufficientFunds = false, ) } @@ -141,18 +151,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, @@ -211,7 +210,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 +234,7 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, swapProvider: SwapProvider, bestRatedProviderId: String, isNeedBestRateBadge: Boolean, @@ -243,9 +243,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, ) @@ -304,7 +305,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 +316,7 @@ internal class StateBuilder( ), fee = feeState, swapButton = SwapButton( - enabled = getSwapButtonEnabled(quoteModel), + enabled = getSwapButtonEnabled(notifications), onClick = actions.onSwapClick, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -363,281 +365,17 @@ internal class StateBuilder( ) } - @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 +391,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 +436,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 +490,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 +528,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, @@ -1002,19 +688,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 +726,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 +878,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 +936,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 +945,7 @@ internal class StateBuilder( ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1303,7 +956,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 +980,7 @@ internal class StateBuilder( ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1368,21 +1021,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, @@ -1405,7 +1043,7 @@ internal class StateBuilder( ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, + isShown = true, onDismissRequest = onDismiss, content = config, ), @@ -1420,21 +1058,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 +1106,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 +1219,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 +1234,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 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..8fb11f33fa 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, @@ -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,7 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + isInsufficientFunds = false, 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/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..512577aaaa 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, ) 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..a43e7c530c 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 @@ -25,10 +25,7 @@ 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.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -44,8 +41,8 @@ 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 @@ -81,6 +78,7 @@ 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, @@ -142,6 +140,7 @@ 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 val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) private var swapRouter: SwapRouter by Delegates.notNull() private val isUserResolvableError: (SwapState) -> Boolean = { @@ -310,6 +309,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromCurrencyStatus, toToken = toCurrencyStatus, amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus), ) } @@ -327,6 +327,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: String, + reduceBalanceBy: BigDecimal, toProvidersList: List, isSilent: Boolean = false, ) { @@ -345,6 +346,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, + reduceBalanceBy = reduceBalanceBy, toProvidersList = toProvidersList, ), ) @@ -360,6 +362,7 @@ internal class SwapViewModel @Inject constructor( toToken = toCurrency, amount = amount, isSilent = isSilent, + reduceBalanceBy = dataState.reduceBalanceBy, toProvidersList = findSwapProviders(fromCurrency, toCurrency), ) } @@ -369,6 +372,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: String, + reduceBalanceBy: BigDecimal, toProvidersList: List, ): PeriodicTask> { return PeriodicTask( @@ -378,6 +382,7 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, + reduceBalanceBy = reduceBalanceBy, swapDataModel = null, approveDataModel = null, ) @@ -386,6 +391,7 @@ internal class SwapViewModel @Inject constructor( toToken = toToken, providers = toProvidersList, amountToSwap = amount, + reduceBalanceBy = reduceBalanceBy, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } @@ -408,7 +414,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 +429,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, @@ -822,6 +829,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 +854,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 +899,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 +911,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 +934,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 +948,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 +969,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 +1024,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 +1035,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 +1063,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 -> 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/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/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 6fd2e7e7e9..d03303bda6 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,7 +10,6 @@ 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 @@ -20,6 +19,7 @@ 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 @@ -68,7 +68,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() @@ -82,7 +82,7 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { private fun TopAppBar(state: FeatureTogglesContentState.TopBarState, onBackClick: () -> Unit) { AppBarWithBackButtonAndIcon( onBackClick = onBackClick, - text = stringResource(id = R.string.feature_toggles), + text = stringResourceSafe(id = R.string.feature_toggles), iconRes = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) { R.drawable.ic_refresh_24 } else { 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..6f280851dc --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.tester.presentation.providers.entity + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.network.providers.ProviderType +import kotlinx.collections.immutable.ImmutableList + +internal data class BlockchainProvidersUM( + val onBackClick: () -> Unit, + val blockchainProviders: ImmutableList, +) { + + @Immutable + data class ProvidersUM( + val blockchainId: String, + val blockchainName: String, + val blockchainSymbol: String, + val providers: ImmutableList, + val isExpanded: Boolean, + val onDrop: (id: String, prev: Int, current: Int) -> Unit, + ) + + @Immutable + data class ProviderUM(val type: ProviderType) { + + val name: String = when (type) { + is ProviderType.Public -> type.url + else -> type::class.java.simpleName + } + } +} \ 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..32691867a6 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt @@ -0,0 +1,268 @@ +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.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.rows.RowText +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R +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 + +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 { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.blockchain_providers), + containerColor = TangemTheme.colors.background.primary, + ) + } + + items( + items = state.blockchainProviders, + key = ProvidersUM::blockchainId, + contentType = { "BlockchainProviders" }, + itemContent = { BlockchainProvidersItem(it) }, + ) + } + } +} + +@Composable +private fun BlockchainProvidersItem(state: ProvidersUM) { + var isExpanded by remember { mutableStateOf(state.isExpanded) } + + ExpandableBlockchainRow( + state = state, + modifier = Modifier.clickable { isExpanded = !isExpanded }, + content = { Providers(state = state, isExpanded = isExpanded) }, + ) +} + +@Composable +private fun ExpandableBlockchainRow( + state: ProvidersUM, + content: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + BlockchainRow(state) + + content() + } +} + +@Composable +private fun BlockchainRow(state: ProvidersUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.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, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + label = "providers_visibility", + ) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + state.providers.fastForEachIndexed { index, provider -> + key("${provider.name}_$index") { + ProviderItem( + index = index, + state = provider, + onDrop = { prev, new -> state.onDrop(state.blockchainId, prev, new) }, + ) + + if (index == state.providers.lastIndex) SpacerH8() + } + } + } + } +} + +@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, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${index + 1}.", + color = TangemTheme.colors.text.accent, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) + + 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, + ) + } + } +} + +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 + } +} \ 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..f8f6981bc8 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.tester.presentation.providers.viewmodel + +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.feature.tester.presentation.navigation.InnerTesterRouter +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 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(onBackClick = {}, blockchainProviders = persistentListOf()), + ) + + private val manager = blockchainProvidersTypesManager as MutableBlockchainProvidersTypesManager + + init { + subscribeOnBlockchainProviderTypesUpdates() + } + + fun setupNavigation(router: InnerTesterRouter) { + _state.update { state -> + state.copy(onBackClick = router::back) + } + } + + private fun subscribeOnBlockchainProviderTypesUpdates() { + manager.get() + .onEach { blockchainProviderTypes -> + _state.update { state -> + state.copy(blockchainProviders = blockchainProviderTypes.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(), + onDrop = ::onDrop, + isExpanded = false, + ) + } + .toImmutableList() + } + + 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.copy( + blockchainProviders = it.blockchainProviders.map { block -> + if (block.blockchainId == id) { + block.copy(providers = providers.toImmutableList()) + } else { + block + } + } + .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 { + return toMutableList().apply { + set(prev, this[current]) + set(current, this[prev]) + } + } +} \ 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..ae85c60e3b 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -11,4 +11,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/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/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 5e15fa4efc..cc941824dc 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 @@ -249,12 +249,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 +262,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 +275,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 +288,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), ) } 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..097f21a1b0 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 @@ -32,7 +32,6 @@ 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 @@ -65,16 +64,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon 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 -> 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..988622e848 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 @@ -12,6 +12,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam 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 @@ -31,7 +32,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase @@ -116,6 +117,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val clipboardManager: ClipboardManager, private val getCryptoCurrencySyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val onrampFeatureToggles: OnrampFeatureToggles, + private val shareManager: ShareManager, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, @@ -219,6 +221,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 +557,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,7 +596,7 @@ internal class TokenDetailsViewModel @Inject constructor( ) if (extendedKey.isNotBlank()) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = extendedKey) + clipboardManager.setText(text = extendedKey, isSensitive = true) internalUiState.value = stateFactory.getStateAndTriggerEvent( state = internalUiState.value, errorMessage = resourceReference(R.string.wallet_notification_address_copied), @@ -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..5ac744ef62 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 @@ -15,6 +14,7 @@ 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.extensions.stringResourceSafe import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.util.cardTypesResolver @@ -96,10 +96,10 @@ internal class WalletSettingsModel @Inject constructor( messageSender.send( ContentMessage { onDismiss -> BasicDialog( - message = stringResource(R.string.user_wallet_list_delete_prompt), + message = stringResourceSafe(R.string.user_wallet_list_delete_prompt), onDismissDialog = onDismiss, confirmButton = DialogButtonUM( - title = stringResource(R.string.common_delete), + title = stringResourceSafe(R.string.common_delete), warning = true, onClick = { forgetWallet() @@ -107,7 +107,7 @@ internal class WalletSettingsModel @Inject constructor( }, ), dismissButton = DialogButtonUM( - title = stringResource(R.string.common_cancel), + title = stringResourceSafe(R.string.common_cancel), onClick = onDismiss, ), ) 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/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/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 49c11b5799..e4293a0ec9 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..a2fa5ca344 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 @@ -113,6 +114,7 @@ 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, @@ -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) } } } 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 b0eb7a25de..911961ff91 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-888" +tangemBlockchainSdk = "develop-896" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.19-419" +tangemCardSdk = "develop-420" #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..e40d856618 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,6 +143,8 @@ 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 @@ -250,6 +253,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,6 +288,8 @@ 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" @@ -358,6 +364,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,6 +384,7 @@ 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" 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/settings.gradle.kts b/settings.gradle.kts index 1db213040e..d5fda70b8a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -245,6 +245,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 +270,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