diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt index 16a05117ac..3ef77ac909 100644 --- a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt +++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt @@ -1,8 +1,32 @@ package com.tangem.tap import android.content.Intent +import android.hardware.biometrics.BiometricManager +import android.os.Build +import android.provider.Settings import androidx.activity.result.ActivityResultLauncher interface ActivityResultCaller { val activityResultLauncher: ActivityResultLauncher? +} + +internal fun ActivityResultCaller.openSystemBiometrySettings() { + val settingsAction = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + Settings.ACTION_BIOMETRIC_ENROLL + } + else -> { + Settings.ACTION_SECURITY_SETTINGS + } + } + val intent = Intent(settingsAction).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG, + ) + } + } + + activityResultLauncher?.launch(intent) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index c86e0f9392..45dae620de 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -4,6 +4,8 @@ import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager @@ -110,4 +112,8 @@ interface ApplicationEntryPoint { fun getDetailsFeatureToggles(): DetailsFeatureToggles fun getDetailsEntryPoint(): DetailsEntryPoint + + fun getUrlOpener(): UrlOpener + + fun getShareManager(): ShareManager } \ 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 new file mode 100644 index 0000000000..d76bd6cd67 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -0,0 +1,61 @@ +package com.tangem.tap + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.Fragment +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.utils.Provider +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class DecomposeFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + private val component by lazy(mode = LazyThreadSafetyMode.NONE) { + requireNotNull(componentBuilder?.build()) { + "Component builder is not set, call newInstance() for DecomposeFragment creation first." + } + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + component.Content(modifier) + + EventMessageEffect( + messageHandler = uiDependencies.eventMessageHandler, + snackbarHostState = uiDependencies.globalSnackbarHostState, + ) + } + + private class ComponentBuilder>( + private val contextProvider: Provider, + private val params: P, + private val componentFactory: F, + ) { + + fun build(): C = componentFactory.create(contextProvider(), params) + } + + companion object { + + private var componentBuilder: ComponentBuilder<*, *, *>? = null + + fun > newInstance( + contextProvider: Provider, + params: P, + componentFactory: F, + ): Fragment { + this@Companion.componentBuilder = ComponentBuilder(contextProvider, params, componentFactory) + + return DecomposeFragment() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index a267125f20..70192d4bb6 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -298,19 +298,21 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - val pendingIntent = PendingIntent.getActivity( - this, - 0, - Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, - ) - val intentFilters = arrayOf( - IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), - ) - nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + if (nfcAdapter?.isEnabled == true) { + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + val intentFilters = arrayOf( + IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), + ) + nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + } // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) @@ -319,8 +321,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onPause() { super.onPause() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - nfcAdapter.disableForegroundDispatch(this) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + + if (nfcAdapter?.isEnabled == true) { + nfcAdapter.disableForegroundDispatch(this) + } } override fun onStop() { diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 040fb176a2..414c48552b 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -53,7 +53,7 @@ import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHand import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.common.log.TimberFormatStrategy @@ -181,7 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase get() = entryPoint.getSaveBlockchainErrorUseCase() - // endregion private val detailsFeatureToggles: DetailsFeatureToggles get() = entryPoint.getDetailsFeatureToggles() @@ -189,6 +188,13 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val detailsEntryPoint: DetailsEntryPoint get() = entryPoint.getDetailsEntryPoint() + private val urlOpener + get() = entryPoint.getUrlOpener() + + private val shareManager + get() = entryPoint.getShareManager() + // endregion + override fun onCreate() { super.onCreate() @@ -276,6 +282,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, detailsFeatureToggles = detailsFeatureToggles, detailsEntryPoint = detailsEntryPoint, + urlOpener = urlOpener, + shareManager = shareManager, ), ), ) @@ -363,7 +371,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector, ) - val feedbackManager = FeedbackManager( + val feedbackManager = LegacyFeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, chatManager = ChatManager(foregroundActivityObserver), diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 6b870386be..1f9043292e 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common import android.app.Dialog import android.content.Context -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.ui.* diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index d16d3488e2..6c7ab7e524 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,8 +1,7 @@ package com.tangem.tap.common.extensions -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.StateDialog import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -82,12 +81,12 @@ suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } -fun Store<*>.dispatchOpenUrl(url: String) { - dispatch(NavigationAction.OpenUrl(url)) +fun Store.dispatchOpenUrl(url: String) { + inject(DaggerGraphState::urlOpener).openUrl(url) } -fun Store<*>.dispatchShare(url: String) { - dispatch(NavigationAction.Share(url)) +fun Store.dispatchShare(url: String) { + inject(DaggerGraphState::shareManager).shareText(url) } inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { 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 524edaded8..b14e9e4090 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 @@ -3,7 +3,10 @@ package com.tangem.tap.common.extensions import android.app.Activity -import android.content.* +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.* @@ -78,16 +81,6 @@ fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? { return clipData.getItemAt(0).text } -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(shareIntent) -} - 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/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt rename to app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index 98aa013e84..ed99833b04 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -25,7 +25,7 @@ import java.io.StringWriter /** [REDACTED_AUTHOR] */ -class FeedbackManager( +class LegacyFeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val chatManager: ChatManager, @@ -144,7 +144,7 @@ class FeedbackManager( } } - companion object { + private companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" const val FEEDBACK_FILE = "feedback.txt" diff --git a/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt new file mode 100644 index 0000000000..680468bef1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.common.feedback + +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.feedback.FeedbackType +import com.tangem.tap.store +import timber.log.Timber + +internal class ProxyFeedbackManager : FeedbackManager { + + override fun sendEmail(type: FeedbackType) { + val manager = store.state.globalState.feedbackManager + + if (manager == null) { + Timber.e("Feedback manager is not initialized") + return + } + + val data = when (type) { + is FeedbackType.Feedback -> FeedbackEmail() + is FeedbackType.RateCanBeBetter -> RateCanBeBetterEmail() + is FeedbackType.ScanFails -> ScanFailsEmail() + is FeedbackType.SendTransactionFailed -> SendTransactionFailedEmail(type.error) + is FeedbackType.Support -> FeedbackEmail() + } + + manager.sendEmail(data) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt new file mode 100644 index 0000000000..dc15eb3e95 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.finisher + +import android.content.Context +import android.content.Intent +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.tap.MainActivity +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class AndroidAppFinisher( + private val appContext: Context, +) : AppFinisher { + + override fun finish() { + foregroundActivityObserver.withForegroundActivity { activity -> + activity.finish() + } + } + + override fun restart() { + val intent = Intent(appContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + appContext.startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index fc362509c3..b896303e37 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.redux import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.TestAction import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.WalletAddressData 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 8439279a0a..4aa4c0b58b 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 @@ -2,14 +2,14 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.FeedbackData -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction @@ -79,7 +79,7 @@ sealed class GlobalAction : Action { data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() - data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() + data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() 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 fc584f6b8e..f1d80acbf8 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 @@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.models.Config import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.LogConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.SendAction diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 7a75d16d87..50f8f85f47 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,11 +1,11 @@ package com.tangem.tap.common.redux.global -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.onboarding.OnboardingManager @@ -20,7 +20,7 @@ data class GlobalState( val tapWalletManager: TapWalletManager = TapWalletManager(), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, - val feedbackManager: FeedbackManager? = null, + val feedbackManager: LegacyFeedbackManager? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index ad2b8f3178..00f61f18db 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -1,14 +1,11 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent -import android.hardware.biometrics.BiometricManager -import android.os.Build -import android.provider.Settings import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.activityResultCaller -import com.tangem.tap.common.CustomTabsManager -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.openFragment +import com.tangem.tap.common.extensions.popBackTo import com.tangem.tap.common.redux.AppState import com.tangem.tap.store import org.rekotlin.Middleware @@ -47,39 +44,11 @@ val navigationMiddleware: Middleware = { _, state -> } } } - is NavigationAction.OpenUrl -> { - navState?.activity?.get()?.let { - CustomTabsManager().openUrl(action.url, it) - } - } is NavigationAction.OpenDocument -> { val intent = Intent(Intent.ACTION_VIEW) intent.data = action.url navState?.activity?.get()?.startActivity(intent) } - is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog) - is NavigationAction.OpenBiometricsSettings -> { - val settingsAction = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - Settings.ACTION_BIOMETRIC_ENROLL - } - else -> { - Settings.ACTION_SECURITY_SETTINGS - } - } - val intent = Intent(settingsAction).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - putExtra( - Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, - BiometricManager.Authenticators.BIOMETRIC_STRONG, - ) - } - } - activityResultCaller.activityResultLauncher?.launch(intent) - } - is NavigationAction.Share -> { - navState?.activity?.get()?.shareText(action.data) - } is NavigationAction.ActivityCreated, is NavigationAction.ActivityDestroyed, -> Unit diff --git a/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt new file mode 100644 index 0000000000..51b829de5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.common.share + +import android.content.Intent +import com.tangem.core.navigation.share.ShareManager +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class IntentShareManager : ShareManager { + + override fun shareText(text: String) { + foregroundActivityObserver.withForegroundActivity { activity -> + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + + activity.startActivity(shareIntent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt index 64763a6216..5b0ac2e000 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -44,7 +43,7 @@ class RussianCardholdersWarningBottomSheetDialog( dismiss() } binding?.btnNo?.setOnClickListener { - store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL)) + store.dispatchOpenUrl(INSTRUCTION_URL) dismiss() } } diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index d59f668687..33c8044a23 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -6,7 +6,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.redux.global.GlobalAction diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt similarity index 74% rename from app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt rename to app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index c56c238e57..549f514b01 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common +package com.tangem.tap.common.url import android.content.Context import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -8,12 +8,22 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -class CustomTabsManager { - fun openUrl(url: String, context: Context) { +internal class CustomTabsUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + foregroundActivityObserver.withForegroundActivity { + openUrl(url, context = it) + } + } + + private fun openUrl(url: String, context: Context) { if (url.isEmpty()) return val customTabsIntent = CustomTabsIntent.Builder() .setDefaultColorSchemeParams( diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index cbf8eebb27..62f28c043e 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -6,10 +6,8 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.tap.MainActivity -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder import dagger.Module @@ -67,10 +65,4 @@ internal object ActivityModule { ): GetPolkadotCheckHasImmortalUseCase { return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository) } - - @Provides - @Singleton - fun provideActivityClassWrapper(): ActivityClassWrapper { - return ActivityClassWrapper(MainActivity::class.java) - } } \ 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 b23a41b8a8..1128b32708 100644 --- a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt +++ b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt @@ -7,8 +7,7 @@ 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.RootAppComponentContext -import com.tangem.core.decompose.ui.UiMessage -import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.ui.UiDependencies import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -16,7 +15,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped -import timber.log.Timber @Module @InstallIn(ActivityComponent::class) @@ -29,17 +27,11 @@ internal object RootAppComponentContextModule { @ActivityContext context: Context, dispatchers: CoroutineDispatcherProvider, componentBuilder: DecomposeComponent.Builder, + uiDependencies: UiDependencies, ): AppComponentContext { - // TODO: Implement message handler - val dummyMessageHandler = object : UiMessageHandler { - override fun handleMessage(message: UiMessage) { - Timber.w("Unable to handle message: $message") - } - } - return DefaultAppComponentContext( componentContext = (context as AppCompatActivity).defaultComponentContext(), - messageHandler = dummyMessageHandler, + messageHandler = uiDependencies.eventMessageHandler, dispatchers = dispatchers, hiltComponentBuilder = componentBuilder, ) 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 97b0f48bee..22a522e553 100644 --- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di +import androidx.compose.material3.SnackbarHostState import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.Module import dagger.Provides @@ -19,6 +21,8 @@ internal object UiDependenciesModule { return object : UiDependencies { override val hapticManager = hapticManager override val appThemeModeHolder = appThemeModeHolder + override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() + override val eventMessageHandler: EventMessageHandler = EventMessageHandler() } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt new file mode 100644 index 0000000000..d5565743e9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.tap.common.feedback.ProxyFeedbackManager +import com.tangem.tap.common.finisher.AndroidAppFinisher +import com.tangem.tap.common.share.IntentShareManager +import com.tangem.tap.common.url.CustomTabsUrlOpener +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UtilsModule { + + @Provides + @Singleton + fun provideShareManager(): ShareManager = IntentShareManager() + + @Provides + @Singleton + fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener() + + @Provides + @Singleton + fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager() + + @Provides + @Singleton + fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index ed777bd666..bdb9786c3c 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -8,9 +8,9 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.StateDialog import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 372889fe91..a9f6bdca6d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.* @@ -30,9 +31,6 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R @@ -273,7 +271,7 @@ class DetailsMiddleware { private fun enrollBiometrics() { Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun changeAppThemeMode(appThemeMode: AppThemeMode) { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 750f4b04c5..65d11fc520 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.core.navigation.StateDialog import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 95a6865b3c..e7ac985d9f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState @@ -179,7 +180,7 @@ internal class DetailsViewModel( private fun handleSocialNetworkClick(link: SocialNetworkLink) { Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatchOnMain(NavigationAction.OpenUrl(link.url)) + store.dispatchOpenUrl(link.url) } private fun getSocialLinks(): ImmutableList { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 0134ddaf15..97f17bd9b5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 64b81fc771..6663311df8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.graphics.Bitmap import android.net.Uri -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import org.rekotlin.StateType /** diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 45eb5225a2..932c1a1965 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -8,6 +8,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding @@ -16,11 +17,7 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch @@ -88,7 +85,7 @@ internal class SaveWalletMiddleware { } private fun enrollBiometrics() { - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun allowToUseBiometrics(state: SaveWalletState) { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 3c3409459e..5663b7cec7 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.redux.ToastNotificationAction diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index d3d3cd7e19..115ab92034 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero -import com.tangem.core.navigation.StateDialog import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.CurrencyConverter import com.tangem.tap.common.entities.IndeterminateProgressButton diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cf7de3bfe1..0e1a98b9c7 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -6,13 +6,13 @@ import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.ExchangeService import org.rekotlin.Action @@ -57,7 +57,7 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde mainStore?.onUserWalletSelected(userWallet) } - override fun sendFeedbackEmail() { - mainStore?.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + override fun dispatchDialogShow(dialog: StateDialog) { + mainStore?.dispatchDialogShow(dialog) } } \ 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 3dc61535c0..08d6c0997f 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 @@ -3,6 +3,8 @@ package com.tangem.tap.proxy.redux import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.navigation.email.EmailSender +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -34,8 +36,8 @@ import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -81,4 +83,6 @@ data class DaggerGraphState( val detailsFeatureToggles: DetailsFeatureToggles? = null, val detailsEntryPoint: DetailsEntryPoint? = null, val stakingRouter: StakingRouter? = null, + val urlOpener: UrlOpener? = null, + val shareManager: ShareManager? = null, ) : StateType \ No newline at end of file diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 62c6c0de87..319c15eebc 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(deps.tangem.vico.compose) implementation(deps.tangem.vico.compose.m3) + implementation(deps.lifecycle.compose) implementation(deps.compose.foundation) implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 31f0f55f2b..9bbaeaa48b 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -1,8 +1,402 @@ package com.tangem.common.ui.charts -import androidx.compose.runtime.Composable +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontSynthesis +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.resolveAsTypeface +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.axis.* +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.component.LineComponent +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.layer.rememberMarketChartLayer +import com.tangem.common.ui.charts.marker.rememberTangemChartMarker +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider +import com.tangem.common.ui.charts.state.* +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.RoundingMode + +private const val GUIDELINES_COUNT = 3 + +/** + * MarketChart ui component for representing coin prices. + * + * @param modifier The modifier to be applied to the chart. + * @param state The state of the Market Chart, which includes data and look of the chart. + * @param splitChartSegmentColor The color of the grayed by marker chart segment. + * @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor] + * @param backgroundColorAlpha The alpha of the background color of the chart. + * @param noChartContent A composable function that defines the content to be displayed when there is no data to display. + */ +@Composable +fun MarketChart( + modifier: Modifier = Modifier, + state: MarketChartState = rememberMarketChartState(), + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + noChartContent: @Composable BoxScope.() -> Unit, +) { + var canvasWidth by remember { mutableIntStateOf(0) } + var canvasHeight by remember { mutableIntStateOf(0) } + + val layer = rememberLayerFromState( + state = state, + splitChartSegmentColor = splitChartSegmentColor, + backgroundColorAlpha = backgroundColorAlpha, + backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha, + canvasHeight = canvasHeight, + ) + val chart = rememberCartesianChart( + layer, + startAxis = rememberMarketChartStartAxis( + yValueFormatter = state.yValueFormatter, + ), + bottomAxis = rememberMarketChartBottomAxis( + xValueFormatter = state.xValueFormatter, + ), + ) + val marker = rememberTangemChartMarker( + color = state.chartColor, + innerCircleColor = Color.White, + ) + val density = LocalDensity.current + + CartesianChartHost( + modifier = modifier.onGloballyPositioned { + with(density) { + canvasWidth = it.size.width + canvasHeight = if (it.size.height != 0) { + // FIXME get height bounded to min max chart points + it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt() + } else { + 0 + } + } + }, + chart = chart, + modelProducer = state.modelProducer, + scrollState = rememberVicoScrollState(scrollEnabled = false), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth), + diffAnimationSpec = null, + marker = marker, + placeholder = noChartContent, + ) +} @Composable -fun MarketChart() { - TODO() -} \ No newline at end of file +private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener { + val state = this + return remember(state.markerVisibilityListener, canvasWidth) { + val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f } + + object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + state.stopDrawingAnimation() + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onShown(marker, targets) + } + + override fun onHidden(marker: CartesianMarker) { + state.markerFraction = null + state.markerVisibilityListener.onHidden(marker) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onUpdated(marker, targets) + } + } + } +} + +@Composable +private fun rememberLayerFromState( + state: MarketChartState, + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + canvasHeight: Int, +): LineCartesianLayer { + return rememberMarketChartLayer( + lineColor = state.chartColor, + backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha), + secondLineColor = splitChartSegmentColor, + backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha), + secondColorOnTheRightSide = state.markerHighlightRightSide.not(), + startDrawingAnimation = state.startDrawingAnimationState, + markerFraction = state.markerFraction, + axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ? + canvasHeight = canvasHeight, + ) +} + +@Composable +private fun rememberMarketChartStartAxis( + yValueFormatter: CartesianValueFormatter, +): VerticalAxis { + return rememberCustomStartAxis( + axis = null, + tick = null, + guideline = null, + labelGuideline = rememberChartAxisGuidelineComponent( + color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f), + ), + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + background = null, + padding = Dimensions.of( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing4, + ), + textSize = TangemTheme.typography.caption2.fontSize, + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, + verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, + itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false), + valueFormatter = yValueFormatter, + ) +} + +@Composable +fun rememberMarketChartBottomAxis( + xValueFormatter: CartesianValueFormatter, +): HorizontalAxis { + return rememberBottomAxis( + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + textSize = TangemTheme.typography.caption2.fontSize, + padding = Dimensions.of(top = TangemTheme.dimens.spacing20), + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + tick = null, + axis = null, + guideline = null, + sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ? + itemPlacer = remember { + AxisItemPlacer.Horizontal.default( + spacing = 25, // FIXME ? + offset = 60, // FIXME ? + shiftExtremeTicks = false, + addExtremeLabelPadding = false, + ) + }, + valueFormatter = xValueFormatter, + ) +} + +@Composable +private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent { + return rememberAxisGuidelineComponent( + color = color, + shape = Shape.Rectangle, + margins = Dimensions( + startDp = TangemTheme.dimens.spacing4.value, + endDp = TangemTheme.dimens.spacing4.value, + topDp = 0f, + bottomDp = 0f, + ), + thickness = TangemTheme.dimens.size2, + ) +} + +@Composable +internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface { + val resolver = LocalFontFamilyResolver.current + return remember(resolver, this) { + resolver.resolveAsTypeface( + fontFamily = this.fontFamily, + fontWeight = this.fontWeight ?: FontWeight.Normal, + fontStyle = this.fontStyle ?: FontStyle.Normal, + fontSynthesis = this.fontSynthesis ?: FontSynthesis.All, + ) + }.value +} + +// region Preview + +@Suppress("LongMethod") +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MarketChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second + val x = previewData.first + + val dataProducer = remember { + MarketChartDataProducer.build { + chartLook = MarketChartLook( + type = MarketChartLook.Type.Growing, + markerHighlightRightSide = true, + animationOnDataChange = true, + ) + } + } + + LaunchedEffect(key1 = Unit) { + dataProducer.runTransactionSuspend { + chartData = MarketChartData.Data( + x = x, + y = y, + ) + updateLook { + it.copy( + xAxisFormatter = { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + }, + yAxisFormatter = { value -> + value.setScale(3, RoundingMode.HALF_UP).toPlainString() + }, + ) + } + } + } + var markerPoint by remember { + mutableStateOf(Pair(null, null)) + } + + val coroutineScope = rememberCoroutineScope() + val look by dataProducer.lookState.collectAsState() + + TangemThemePreview { + val growingColor = TangemTheme.colors.icon.accent + val fallingColor = TangemTheme.colors.icon.warning + + val chartState = rememberMarketChartState( + dataProducer = dataProducer, + onMarkerShown = { x, y -> + markerPoint = Pair(x, y) + }, + colorMapper = { + when (it) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } + }, + ) + + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) { + Text(text = "Point: ${markerPoint.first}, ${markerPoint.second}") + + MarketChart( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary) + .height(173.dp), + state = chartState, + splitChartSegmentColor = TangemTheme.colors.icon.inactive, + backgroundSplitChartSegmentColorAlpha = 0.24f, + backgroundColorAlpha = 0.24f, + noChartContent = { }, + ) + SpacerH16() + + Button(onClick = { chartState.startDrawingAnimation() }) { + Text("Start drawing animation") + } + Button( + onClick = { + dataProducer.runTransaction { + updateLook { + it.copy(markerHighlightRightSide = !it.markerHighlightRightSide) + } + } + }, + ) { + Text( + text = "Change marker highlight side", + ) + } + Button(onClick = { + coroutineScope.launch { + dataProducer.runTransactionSuspend { + updateData { + MarketChartData.Data( + x = it.x, + y = it.y.reversed(), + ) + } + } + } + },) { + Text("Change Data") + } + Button(onClick = { + dataProducer.runTransaction { + updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } + } + },) { + Text("Change animationOnDataChange = ${look.animationOnDataChange}") + } + + Button(onClick = { + dataProducer.runTransaction { + updateLook { + it.copy( + type = if (it.type == MarketChartLook.Type.Growing) { + MarketChartLook.Type.Falling + } else { + MarketChartLook.Type.Growing + }, + ) + } + } + },) { + Text("Change color type") + } + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt index 239f0f0665..b224a1838d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.fullWidth @@ -27,13 +28,15 @@ import com.patrykandpatrick.vico.compose.common.shader.BrushShader import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal /** * Creates and remembers a LineCartesianLayer for a chart with specific characteristics. @@ -51,19 +54,21 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Suppress("LongParameterList") @Composable -fun rememberMarketChartLayer( +internal fun rememberMarketChartLayer( lineColor: Color, backgroundLineColor: Color, secondLineColor: Color, backgroundSecondLineColor: Color, startDrawingAnimation: MutableState, axisValueOverrider: AxisValueOverrider, - secondColorOnTheRightSide: Boolean = false, - @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, + secondColorOnTheRightSide: Boolean, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float?, + canvasHeight: Int, ): LineCartesianLayer { var animationFraction: Float? by remember { mutableStateOf(null) } LaunchedEffect(startDrawingAnimation.value) { + animationFraction = null if (startDrawingAnimation.value) { animate( initialValue = 0f, @@ -89,9 +94,11 @@ fun rememberMarketChartLayer( secondColorOnTheRightSide = secondColorOnTheRightSide, markerFraction = markerFraction, animationFraction = animationFraction, + canvasHeight = canvasHeight, ) } +@Suppress("LongParameterList") @Composable private fun rememberRawMarketChartLayer( lineColor: Color, @@ -99,6 +106,7 @@ private fun rememberRawMarketChartLayer( secondLineColor: Color, backgroundSecondLineColor: Color, axisValueOverrider: AxisValueOverrider, + canvasHeight: Int, secondColorOnTheRightSide: Boolean = false, @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, @FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null, @@ -109,38 +117,36 @@ private fun rememberRawMarketChartLayer( val markerSet = markerFraction != null val animationRunning = animationFraction != null && animationFraction != 1f - val layerColors = remember(animationRunning, markerSet, secondColorOnTheRightSide) { - when { - !animationRunning && markerSet && secondColorOnTheRightSide -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = secondLineColor, - backLineColorRight = backgroundSecondLineColorGradient, - ) - } - !animationRunning && markerSet && !secondColorOnTheRightSide -> { - LayerColors( - lineColor = secondLineColor, - backLineColor = backgroundSecondLineColorGradient, - lineColorRight = lineColor, - backLineColorRight = backgroundColorLineGradient, - ) - } - animationRunning -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = Color.Transparent, - backLineColorRight = listOf(Color.Transparent, Color.Transparent), - ) - } - else -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - ) - } + val layerColors = when { + !animationRunning && markerSet && secondColorOnTheRightSide -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = secondLineColor, + backLineColorRight = backgroundSecondLineColorGradient, + ) + } + !animationRunning && markerSet && !secondColorOnTheRightSide -> { + LayerColors( + lineColor = secondLineColor, + backLineColor = backgroundSecondLineColorGradient, + lineColorRight = lineColor, + backLineColorRight = backgroundColorLineGradient, + ) + } + animationRunning -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = Color.Transparent, + backLineColorRight = listOf(Color.Transparent, Color.Transparent), + ) + } + else -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + ) } } @@ -148,6 +154,7 @@ private fun rememberRawMarketChartLayer( fractionValue = animationFraction ?: markerFraction, axisValueOverrider = axisValueOverrider, layerColors = layerColors, + canvasHeight = canvasHeight, ) } @@ -163,14 +170,26 @@ private fun rememberLayer( fractionValue: Float?, axisValueOverrider: AxisValueOverrider, layerColors: LayerColors, + canvasHeight: Int, ): LineCartesianLayer { + val endGradientColorPosition = if (canvasHeight != 0) { + canvasHeight * END_GRADIENT_COLOR_POSITION_PERCENTAGE + } else { + Float.POSITIVE_INFINITY + } + return rememberLineCartesianLayer( listOf( if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) { rememberLineSpec( shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) }, - backgroundShader = remember(layerColors.backLineColor) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) }, ) } else { @@ -181,11 +200,21 @@ private fun rememberLayer( positions = floatArrayOf(fractionValue, fractionValue), ) }, - backgroundShaderFirst = remember(layerColors.backLineColor) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) }, - backgroundShaderSecond = remember(layerColors.backLineColorRight) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColorRight)) + backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColorRight, + endY = endGradientColorPosition, + ), + ) }, xSplitFraction = fractionValue, ) @@ -195,14 +224,23 @@ private fun rememberLayer( ) } +private const val END_GRADIENT_COLOR_POSITION_PERCENTAGE = 0.9f + // region Preview +@Suppress("LongMethod") @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun LayerChartPreview() { - val xListKey = ExtraStore.Key>() - val model = getPreviewBigCartData(xListKey) +private fun LayerChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + var lineColor by remember { + mutableStateOf(Color.Blue) + } TangemThemePreview { Column( @@ -213,12 +251,13 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), secondColorOnTheRightSide = true, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), @@ -230,13 +269,14 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), markerFraction = 0.35f, secondColorOnTheRightSide = true, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), @@ -248,14 +288,34 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), - secondLineColor = Color.Blue, - backgroundSecondLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = false, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = lineColor, + backgroundSecondLineColor = lineColor.copy(alpha = 0.24f), markerFraction = 0.35f, secondColorOnTheRightSide = true, animationFraction = 0.7f, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt index 4675085e2a..e27ce63a70 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.fullWidth @@ -24,16 +25,18 @@ import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.compose.common.shape.dashed import com.patrykandpatrick.vico.core.cartesian.* import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.common.Dimensions import com.patrykandpatrick.vico.core.common.component.TextComponent -import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape -import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal /** * @param color The color of the indicator and guideline. @@ -53,7 +56,11 @@ internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): C ) val indicatorRearComponent = rememberShapeComponent( shape = Shape.Pill, - color = color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), + color = if (color == Color.Transparent) { + Color.Transparent + } else { + color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA) + }, ) val indicator = rememberLayeredComponent( rear = indicatorRearComponent, @@ -105,10 +112,14 @@ private const val INDICATOR_REAR_COLOR_ALPHA = .24f @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TangemChartMarkerPreview() { +private fun TangemChartMarkerPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { val marker = rememberTangemChartMarker(Color.Red, Color.White) - val xListKey = ExtraStore.Key>() - val model = getPreviewBigCartData(xListKey) + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt new file mode 100644 index 0000000000..662cb41bc6 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt @@ -0,0 +1,152 @@ +@file:Suppress("MagicNumber") + +package com.tangem.common.ui.charts.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import java.math.BigDecimal + +internal class MarketChartPreviewDataProvider : PreviewParameterProvider, List>> { + override val values: Sequence, List>> + get() { + val bitcoinPrice = listOf( + 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, + 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, + 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, + 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, + 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, + 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, + 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, + 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, + 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, + 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, + 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, + 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, + 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, + 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, + 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, + 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, + 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, + 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, + ).map { it.toBigDecimal() } + val bitcoinTimestamps = listOf( + 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, + 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, + 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, + 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, + 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, + 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, + 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, + 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, + 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, + 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, + 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, + 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, + 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, + 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, + 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, + 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, + 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, + 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, + 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, + 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, + 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, + 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, + 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, + 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, + 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, + 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, + 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, + 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, + 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, + 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, + 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, + ).map { it.toBigDecimal() } + val notcoinPrice = listOf( + "0.02026708", "0.02033274", "0.02112643", "0.02090579", + "0.02047231", "0.0215645", "0.0089055", "0.00674459", + "0.00733586", "0.00758165", "0.0068592", "0.00680505", + "0.00685493", "0.00702113", "0.00725545", "0.00696515", + "0.00681556", "0.00677416", "0.00669383", "0.00659057", + "0.00668081", "0.0066211", "0.0065232", "0.00612542", + "0.00600531", "0.00570974", "0.00559875", "0.00555247", + "0.005495", "0.00547841", "0.00545514", "0.00553865", + "0.00563302", "0.00568475", "0.00562512", "0.00551265", + "0.005398", "0.00560001", "0.00572701", "0.00563223", + "0.00555209", "0.00549261", "0.00524999", "0.00531381", + "0.00539702", "0.00530647", "0.00533963", "0.00527143", + "0.00525981", "0.00495323", "0.00481584", "0.0048854", + "0.00480298", "0.00471316", "0.00473191", "0.00476389", + "0.00476992", "0.00484649", "0.00471095", "0.00501396", + "0.00495481", "0.00545029", "0.0053947", "0.00533251", + "0.00518064", "0.00504481", "0.00507209", "0.00516952", + "0.00524886", "0.00542661", "0.00544231", "0.00579898", + "0.00681472", "0.00720634", "0.00824023", "0.00856427", + "0.00821144", "0.00818103", "0.00960254", "0.00911172", + "0.00888481", "0.00925911", "0.0091132", "0.009285", + "0.00886256", "0.00938211", "0.00936566", "0.0104387", + "0.01088452", "0.01200575", "0.01217514", "0.011921", + "0.01293908", "0.0126124", "0.01224851", "0.01191149", + "0.01179783", "0.01164533", "0.01175718", "0.01169137", + "0.01212768", "0.01215952", "0.01300256", "0.01589522", + "0.01588223", "0.01780629", "0.01919794", "0.01922187", + "0.02165268", "0.02400163", "0.02290975", "0.02383495", + "0.02088105", "0.02373489", "0.02269442", "0.02226249", + "0.02148038", "0.0232045", "0.02623129", "0.02378975", + "0.02438442", "0.02417507", "0.02269891", "0.02236198", + "0.02209164", "0.02169842", "0.02137774", "0.02188837", + "0.0216619", "0.02238334", "0.02186701", "0.02186521", + "0.0217595", "0.02099916", "0.02129634", "0.02143028", + "0.02192513", "0.02172005", "0.02184525", "0.01873974", + "0.01899164", "0.01957115", "0.0204723", "0.0199247", + "0.01935441", "0.01886976", "0.01856944", "0.0179975", + "0.0178625", "0.01796981", + ).map { BigDecimal(it) } + val notcoinTimestamps = listOf( + 1715428800000, 1715443200000, 1715457600000, 1715472000000, + 1715486400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, + 1715961600000, 1715976000000, 1715990400000, 1716004800000, + 1716019200000, 1716033600000, 1716048000000, 1716062400000, + 1716076800000, 1716091200000, 1716105600000, 1716120000000, + 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, + 1716249600000, 1716264000000, 1716278400000, 1716292800000, + 1716307200000, 1716321600000, 1716336000000, 1716350400000, + 1716364800000, 1716379200000, 1716393600000, 1716408000000, + 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, + 1716537600000, 1716552000000, 1716566400000, 1716580800000, + 1716595200000, 1716609600000, 1716624000000, 1716638400000, + 1716652800000, 1716667200000, 1716681600000, 1716696000000, + 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, + 1716825600000, 1716840000000, 1716854400000, 1716868800000, + 1716883200000, 1716897600000, 1716912000000, 1716926400000, + 1716940800000, 1716955200000, 1716969600000, 1716984000000, + 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, + 1717113600000, 1717128000000, 1717142400000, 1717156800000, + 1717171200000, 1717185600000, 1717200000000, 1717214400000, + 1717228800000, 1717243200000, 1717257600000, 1717272000000, + 1717286400000, 1717300800000, 1717315200000, 1717329600000, + 1717344000000, 1717358400000, 1717372800000, 1717387200000, + 1717401600000, 1717416000000, 1717430400000, 1717444800000, + 1717459200000, 1717473600000, 1717488000000, 1717502400000, + 1717516800000, 1717531200000, 1717545600000, 1717560000000, + 1717574400000, 1717588800000, 1717603200000, 1717617600000, + 1717632000000, 1717646400000, 1717660800000, 1717675200000, + 1717689600000, 1717704000000, 1717718400000, 1717732800000, + 1717747200000, 1717761600000, 1717776000000, 1717790400000, + 1717804800000, 1717819200000, 1717833600000, 1717848000000, + 1717862400000, 1717876800000, 1717891200000, 1717905600000, + 1717920000000, 1717934400000, + ).map { it.toBigDecimal() } + + return sequenceOf(bitcoinTimestamps to bitcoinPrice, notcoinTimestamps to notcoinPrice) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt deleted file mode 100644 index 0b90226888..0000000000 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt +++ /dev/null @@ -1,77 +0,0 @@ -@file:Suppress("MagicNumber", "LongMethod") - -package com.tangem.common.ui.charts.preview - -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel -import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.common.data.ExtraStore -import com.patrykandpatrick.vico.core.common.data.MutableExtraStore - -internal fun getPreviewBigCartData(xListKey: ExtraStore.Key>): CartesianChartModel { - val y = listOf( - 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, - 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, - 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, - 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, - 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, - 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, - 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, - 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, - 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, - 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, - 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, - 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, - 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, - 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, - 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, - 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, - 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, - 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, - ) - val timestamps: List = listOf( - 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, - 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, - 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, - 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, - 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, - 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, - 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, - 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, - 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, - 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, - 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, - 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, - 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, - 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, - 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, - 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, - 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, - 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, - 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, - 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, - 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, - 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, - 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, - 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, - 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, - 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, - 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, - 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, - 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, - 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, - 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, - 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, - 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, - 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, - 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, - 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, - ) - - val x = List(y.size) { index -> index } - - return CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }).copy( - extraStore = MutableExtraStore().apply { - set(xListKey, timestamps) - }, - ) -} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt new file mode 100644 index 0000000000..e01681994c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt @@ -0,0 +1,17 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import java.math.BigDecimal + +/** + * Used for formatting the axis labels in a chart. + * It takes a BigDecimal value and returns a CharSequence that represents the formatted label. + * + * @param value The value to be formatted. + * @return The formatted label as a CharSequence. + */ +@Stable +fun interface AxisLabelFormatter { + + fun format(value: BigDecimal): CharSequence +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt new file mode 100644 index 0000000000..7e4887fedc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt @@ -0,0 +1,36 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Immutable +import java.math.BigDecimal + +@Immutable +sealed interface MarketChartData { + + /** + * This interface represents the state when there is no data for the Market Chart. + */ + @Immutable + sealed interface NoData : MarketChartData { + @Immutable + data object Empty : NoData + + @Immutable + data object Loading : NoData + + @Immutable + data object ErrorAndRetry : NoData + } + + /** + * This data class represents the data for the Market Chart. + * It includes properties for x and y values. + * + * @property x List of x values. + * @property y List of y values. + */ + @Immutable + data class Data( + val x: List = listOf(), + val y: List = listOf(), + ) : MarketChartData +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt new file mode 100644 index 0000000000..da62140e59 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -0,0 +1,207 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * + * @property chartLook The updated look of the Market Chart. + * @property chartData The updated state of the Market Chart. + */ +class Transaction( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + var chartLook: MarketChartLook? = null + var chartData: MarketChartData.NoData? = null + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) { + chartData = block(currentData) + } +} + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * It extends the Transaction class and allows update state by data. + * + * @property chartData The updated state of the Market Chart. + * @property chartLook The updated look of the Market Chart. + */ +class TransactionSuspend( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + internal var nonSuspendTransaction: Transaction? = null + var chartData: MarketChartData? = null + var chartLook: MarketChartLook? + get() = nonSuspendTransaction?.chartLook + set(value) { + if (nonSuspendTransaction == null) { + nonSuspendTransaction = Transaction(currentData, currentLook) + } + nonSuspendTransaction?.chartLook = value + } + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) { + chartData = block(currentData) + } + + internal fun updateData(block: (prev: MarketChartData.Data) -> MarketChartData.Data) { + chartData = when (val currentState = currentData) { + is MarketChartData.Data -> block(currentState) + else -> currentState + } + } +} + +@Stable +class MarketChartDataProducer private constructor( + initialData: MarketChartData, + initialLook: MarketChartLook, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) { + internal val startDrawingAnimation = MutableSharedFlow() + internal val dataState = MutableStateFlow(initialData) + internal val lookState = MutableStateFlow(initialLook) + internal val entries = MutableStateFlow>(emptyList()) + + internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher) + + /** + * This function runs a suspending transaction block to update the state and look of the Market Chart. + */ + suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = + handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block)) + + /** + * This function runs a non-suspending transaction block to update the state and look of the Market Chart. + */ + fun runTransaction(block: Transaction.() -> Unit) = + handleTransaction(transaction = Transaction(dataState.value, lookState.value).apply(block)) + + private suspend fun handleTransactionSuspend(transaction: TransactionSuspend) { + val nonSuspendTransaction = transaction.nonSuspendTransaction + val chartData = transaction.chartData + val oldData = dataState.value + + if (chartData != null) { + dataState.value = chartData + } + + if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) { + if (lookState.value.animationOnDataChange) { + startDrawingAnimation.emit(Unit) + } + withContext(dispatcher) { + val minX = chartData.x.min() + val minY = chartData.y.min() + + val normY = chartData.y.map { normalize(it, minY) } + val normX = chartData.x.map { normalize(it, minX) } + + val entriesLocal = normX.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, normY[index]) } + entries.value = entriesLocal + + modelProducer.runTransaction { + add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal))) + + updateExtras { + it[entriesKey] = entriesLocal + it[xKey] = chartData.x + it[yKey] = chartData.y + } + }.await() + } + } + + nonSuspendTransaction?.let { handleTransaction(it) } + } + + private fun handleTransaction(transaction: Transaction) { + transaction.chartData?.let { + dataState.value = it + } + transaction.chartLook?.let { + lookState.value = it + } + } + + companion object { + internal val entriesKey = ExtraStore.Key>() + internal val xKey = ExtraStore.Key>() + internal val yKey = ExtraStore.Key>() + + private val initialData: MarketChartData = MarketChartData.NoData.Empty + private val initialLook: MarketChartLook = MarketChartLook() + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + suspend fun buildSuspend( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: TransactionSuspend.() -> Unit, + ): MarketChartDataProducer { + val transaction = TransactionSuspend(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = initialData, + initialLook = initialLook, + dispatcher = dispatcher, + ).apply { + handleTransactionSuspend(transaction) + } + } + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a non-suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + fun build( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: Transaction.() -> Unit, + ): MarketChartDataProducer { + val transaction = Transaction(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = transaction.chartData ?: initialData, + initialLook = transaction.chartLook ?: initialLook, + dispatcher = dispatcher, + ) + } + } +} + +private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { + val n = value - min + return if (scale > 2) { + n.movePointRight(scale - 2).toFloat() + } else { + n.toFloat() + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt new file mode 100644 index 0000000000..c9ecaae2c0 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -0,0 +1,28 @@ +package com.tangem.common.ui.charts.state + +/** + * This class represents the look and feel of a Market Chart. + * It includes properties for type, marker highlight, animation on data change, animate data appearance, + * and formatters for x and y axis. + * + * @property type The type of the chart, can be either Growing or Falling. + * @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart. + * @property animationOnDataChange A boolean indicating whether to animate on data change. + * @property animateDataAppearance A boolean indicating whether to animate data appearance. + * @property xAxisFormatter A formatter for the x-axis labels. + * @property yAxisFormatter A formatter for the y-axis labels. + */ +data class MarketChartLook( + val type: Type = Type.Growing, + val markerHighlightRightSide: Boolean = true, + val animationOnDataChange: Boolean = false, + val animateDataAppearance: Boolean = false, + val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, + val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, +) { + + enum class Type { + Growing, + Falling, + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt new file mode 100644 index 0000000000..110161db69 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -0,0 +1,151 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import java.math.BigDecimal + +/** + * MarketChartState used for MarketChart ui component. + * + * @param dataProducer The producer of the data for the Market Chart. + * @param colorMapper A function that maps a MarketChartLook.Type to a Color. + * @param onMarkerShown A callback function that is called when the marker is shown, hidden, or updated. + * @return A MarketChartState. + */ +@Composable +fun rememberMarketChartState( + dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} }, + colorMapper: (MarketChartLook.Type) -> Color = { + when (it) { + MarketChartLook.Type.Growing -> Color.Green + MarketChartLook.Type.Falling -> Color.Red + } + }, + onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> }, +): MarketChartState { + val lookState = dataProducer.lookState.collectAsStateWithLifecycle() + + val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) { + MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown) + } + + LaunchedEffect(Unit) { + dataProducer.startDrawingAnimation.collect { + state.startDrawingAnimation() + } + } + + return state +} + +/** + * Represents the state of a Market Chart. + * + * @property dataProducer The producer of the data for the Market Chart. + * @property lookState The look state of the Market Chart. + * @property colorMapper A function that maps a MarketChartLook.Type to a Color. + * @property markerCallback A callback function that is called when the marker is shown, hidden, or updated. + * @property isDrawingAnimationInProgress A boolean indicating whether the drawing animation is in progress. + */ +@Stable +class MarketChartState internal constructor( + private val dataProducer: MarketChartDataProducer, + private val lookState: State, + private val colorMapper: (MarketChartLook.Type) -> Color, + private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit, +) { + internal val startDrawingAnimationState = mutableStateOf(false) + internal val modelProducer = dataProducer.modelProducer + + internal val chartColor by derivedStateOf { + colorMapper(lookState.value.type) + } + + internal val markerHighlightRightSide by derivedStateOf { + lookState.value.markerHighlightRightSide + } + + internal val xValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + val sMin = state.x.min() + val scale = sMin.scale() + val bVal = if (scale > 2) { + value.toBigDecimal().movePointLeft(scale - 2) + sMin + } else { + value.toBigDecimal() + sMin + } + + lookState.value.xAxisFormatter.format(bVal) + } + } + + internal val yValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + val sMin = state.y.min() + val scale = sMin.scale() + val bVal = if (scale > 2) { + value.toBigDecimal().movePointLeft(scale - 2) + sMin + } else { + value.toBigDecimal() + sMin + } + + lookState.value.yAxisFormatter.format(bVal) + } + } + + internal var markerFraction: Float? by mutableStateOf(null) + + internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + + override fun onHidden(marker: CartesianMarker) { + markerCallback(null, null) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + } + + val isDrawingAnimationInProgress: Boolean by derivedStateOf { + startDrawingAnimationState.value + } + + private fun getPoint(targets: List): Pair? { + val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry + val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null + val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null + val x = state.x.getOrNull(entryIndex) ?: return null + val y = state.y.getOrNull(entryIndex) ?: return null + return x to y + } + + fun startDrawingAnimation() { + startDrawingAnimationState.value = true + } + + fun stopDrawingAnimation() { + startDrawingAnimationState.value = false + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt new file mode 100644 index 0000000000..b365fcea59 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt @@ -0,0 +1,8 @@ +package com.tangem.core.decompose.factory + +import com.tangem.core.decompose.context.AppComponentContext + +interface ComponentFactory { + + fun create(context: AppComponentContext, params: Params): Component +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt index 50e3840442..86c43706a6 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.popWhile import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import kotlin.reflect.KClass internal class DefaultRouter( private val navigationProvider: AppNavigationProvider, @@ -19,6 +20,15 @@ internal class DefaultRouter( navigation.pushNew(route, onComplete) } + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + val newRoutes = routes.toList() + + navigation.navigate( + transformer = { newRoutes }, + onComplete = { newStack, _ -> onComplete(newStack.size == newRoutes.size) }, + ) + } + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { navigation.pop(onComplete) } @@ -29,4 +39,11 @@ internal class DefaultRouter( onComplete = onComplete, ) } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.popWhile( + predicate = { it::class != routeClass }, + onComplete = onComplete, + ) + } } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt new file mode 100644 index 0000000000..77e7a8f59d --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt @@ -0,0 +1,26 @@ +package com.tangem.core.decompose.navigation + +import kotlin.reflect.KClass + +class DummyRouter : Router { + + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt index 3a9b7d93ca..da7f859b8f 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt @@ -1,8 +1,10 @@ package com.tangem.core.decompose.navigation +import kotlin.reflect.KClass + /** * Interface for a router in the application. - * It provides methods for navigating through the application. + * It provides methods for navigating through the application by stack. */ interface Router { @@ -14,6 +16,14 @@ interface Router { */ fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** + * Replaces ***all*** routes in the navigation stack with the specified [routes]. + * + * @param routes The routes to replace the current stack with. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** * Pops the top route from the navigation stack. * @@ -22,10 +32,18 @@ interface Router { fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) /** - * Pops routes from the navigation stack until the specified route is found. + * Pops routes from the navigation stack until the specified [route] is found. * * @param route The route to pop to. * @param onComplete The callback to be invoked when the operation is complete. */ fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. + * + * @param routeClass The route class to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt new file mode 100644 index 0000000000..777cc78199 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt @@ -0,0 +1,11 @@ +package com.tangem.core.decompose.navigation + +/** + * Pops routes from the navigation stack until the specified route [R] is found. + * + * @param R The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ +inline fun Router.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) { + popTo(R::class, onComplete) +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt index 676958ef92..ec50c35bc0 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt @@ -17,16 +17,8 @@ sealed class NavigationAction : Action { data class PopBackTo(val screen: AppScreen? = null, val inclusive: Boolean = false) : NavigationAction() - data class OpenUrl(val url: String) : NavigationAction() - data class OpenDocument(val url: Uri) : NavigationAction() - object OpenBiometricsSettings : NavigationAction() - - data class OpenDialog(val stateDialog: StateDialog) : NavigationAction() - - data class Share(val data: String) : NavigationAction() - data class ActivityCreated(val activity: WeakReference) : NavigationAction() data class ActivityDestroyed(val activity: WeakReference) : NavigationAction() diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt new file mode 100644 index 0000000000..2f92f40ad9 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.feedback + +class DummyFeedbackManager : FeedbackManager { + + override fun sendEmail(type: FeedbackType) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..a76d2704e6 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.feedback + +interface FeedbackManager { + + fun sendEmail(type: FeedbackType) +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt new file mode 100644 index 0000000000..b1470f5e4d --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt @@ -0,0 +1,13 @@ +package com.tangem.core.navigation.feedback + +sealed class FeedbackType { + data object RateCanBeBetter : FeedbackType() + + data object ScanFails : FeedbackType() + + data class SendTransactionFailed(val error: String) : FeedbackType() + + data object Feedback : FeedbackType() + + data object Support : FeedbackType() +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt new file mode 100644 index 0000000000..54785819e1 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.finisher + +interface AppFinisher { + + fun finish() + + fun restart() +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt new file mode 100644 index 0000000000..20769b6995 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.share + +class DummyShareManager : ShareManager { + + override fun shareText(text: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt new file mode 100644 index 0000000000..81146f78f4 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.share + +interface ShareManager { + + fun shareText(text: String) +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt new file mode 100644 index 0000000000..cff4946b40 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.url + +class DummyUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt new file mode 100644 index 0000000000..d78ba1b1eb --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.url + +interface UrlOpener { + + fun openUrl(url: String) +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index eb2b8502e1..d4f811162d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) - implementation(deps.jodatime) + api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt similarity index 86% rename from core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt rename to core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt index 28868a7c18..933ae2fa9d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier @Stable -fun interface ComposableContentProvider { +fun interface ComposableContentComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt index 62e19d4af6..49a90c5af6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -1,11 +1,19 @@ package com.tangem.core.ui +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Stable import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder +@Stable interface UiDependencies { val hapticManager: HapticManager val appThemeModeHolder: AppThemeModeHolder + + val globalSnackbarHostState: SnackbarHostState + + val eventMessageHandler: EventMessageHandler } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt new file mode 100644 index 0000000000..b51d1c0f08 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun BoxWithGradient( + modifier: Modifier = Modifier, + gradient: Brush = BottomGradient, + content: @Composable BoxScope.() -> Unit, +) { + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + + Box(modifier = modifier.fillMaxSize()) { + content() + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size164 + bottomInsetsPx.dp) + .background(gradient), + ) + } +} + +private val BottomGradient: Brush = Brush.verticalGradient( + colors = listOf( + TangemColorPalette.Black.copy(alpha = 0f), + TangemColorPalette.Black.copy(alpha = 0.75f), + TangemColorPalette.Black.copy(alpha = 0.95f), + TangemColorPalette.Black, + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 685358db13..dd23347e63 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,23 +1,52 @@ package com.tangem.core.ui.components.appbar 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.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: TextReference, + modifier: Modifier = Modifier, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, +) { + AppBarWithAdditionalButtons( + text = text.resolveReference(), + startButton = startButton, + endButton = endButton, + modifier = modifier, + ) +} /** * App bar with title and two additional buttons @@ -31,24 +60,33 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun AppBarWithAdditionalButtons( text: String, + modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, ) { Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .heightIn(TangemTheme.dimens.size56) .padding(all = TangemTheme.dimens.spacing16), ) { if (startButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { - Icon( - painter = painterResource(id = startButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterStart) + .clickable( + onClick = startButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } Text( @@ -60,14 +98,23 @@ fun AppBarWithAdditionalButtons( ) if (endButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { - Icon( - painter = painterResource(id = endButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterEnd) + .clickable( + onClick = endButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } } } @@ -87,6 +134,7 @@ private fun Preview_AppBarWithAdditionalButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -102,6 +150,7 @@ private fun Preview_AppBarWithOnlyStartButtons() { iconRes = R.drawable.ic_scan_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -117,6 +166,7 @@ private fun Preview_AppBarWithOnlyEndButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt new file mode 100644 index 0000000000..8112024b54 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.components.showcase + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +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.showcase.model.ShowcaseButtonModel +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param headerIconRes big header icon + * @param headerText header text + * @param showcaseItems list of bullet points + * @param primaryButton primary button + * @param secondaryButton secondary button + * @param modifier compose modifier + * + * @see Figma component + */ +@Composable +fun Showcase( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + primaryButton: ShowcaseButtonModel, + secondaryButton: ShowcaseButtonModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + ShowcaseContent( + headerIconRes = headerIconRes, + headerText = headerText, + showcaseItems = showcaseItems, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterHorizontally), + ) + ShowcaseButtons( + primaryButtonText = primaryButton.buttonText, + onPrimaryClick = primaryButton.onClick, + secondaryButtonText = secondaryButton.buttonText, + onSecondaryClick = secondaryButton.onClick, + ) + } +} + +@Composable +private fun ShowcaseButtons( + primaryButtonText: TextReference, + secondaryButtonText: TextReference, + onPrimaryClick: () -> Unit, + onSecondaryClick: () -> Unit, + hint: TextReference? = null, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PrimaryButton( + text = primaryButtonText.resolveReference(), + onClick = onPrimaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) + SecondaryButton( + text = secondaryButtonText.resolveReference(), + onClick = onSecondaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing16, + ), + ) + hint?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Showcase_Preview() { + TangemThemePreview { + Showcase( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + R.drawable.ic_rocket_launch_24, + resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + R.drawable.ic_storefront_24, + resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}), + secondaryButton = ShowcaseButtonModel(resourceReference(R.string.common_later), {}), + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt new file mode 100644 index 0000000000..336172a208 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt @@ -0,0 +1,71 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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 com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun ShowcaseContent( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier, + ) { + Icon( + painter = painterResource(id = headerIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(TangemTheme.dimens.size56), + ) + Text( + text = headerText.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + ) + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + repeat(showcaseItems.size) { index -> + ShowcaseItem( + iconRes = showcaseItems[index].iconRes, + text = showcaseItems[index].text, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt new file mode 100644 index 0000000000..824144dbf5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) { + Row { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing20), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt new file mode 100644 index 0000000000..602465c864 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.components.showcase.model + +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseButtonModel( + val buttonText: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt new file mode 100644 index 0000000000..f9865aab9e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.components.showcase.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseItemModel( + @DrawableRes val iconRes: Int, + val text: TextReference, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt index 0f4d117dab..782cd2a84f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.message +import androidx.compose.runtime.Stable import com.tangem.core.decompose.ui.UiMessage import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.ui.event.StateEvent @@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow /** * Message handler that is used to show or remove an [EventMessage] in the UI. */ +@Stable class EventMessageHandler( private val events: MutableStateFlow> = MutableStateFlow(consumedEvent()), ) : UiMessageHandler, StateFlow> by events { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 55321fa0b2..863f3e59cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -83,6 +83,7 @@ data class TangemDimens internal constructor( val size96: Dp = 96.dp, val size102: Dp = 102.dp, val size108: Dp = 108.dp, + val size110: Dp = 110.dp, val size116: Dp = 116.dp, val size120: Dp = 120.dp, val size142: Dp = 142.dp, 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 4f3287a86d..e5628c9bf5 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 @@ -3,19 +3,18 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager -// TODO: use isSystemInDarkTheme() for automatic color detection -internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false - @Composable fun TangemTheme( isDark: Boolean = false, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, hapticManager: HapticManager = MockHapticManager, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, content: @Composable () -> Unit, ) { val themeColors = if (isDark) darkThemeColors() else lightThemeColors() @@ -33,6 +32,7 @@ fun TangemTheme( LocalTangemShapes provides shapes, LocalIsInDarkTheme provides isDark, LocalHapticManager provides hapticManager, + LocalSnackbarHostState provides snackbarHostState, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -204,4 +204,8 @@ val LocalIsInDarkTheme = staticCompositionLocalOf { false } val LocalHapticManager = staticCompositionLocalOf { error("No HapticManager provided") +} + +val LocalSnackbarHostState = staticCompositionLocalOf { + error("No SnackbarHostState provided") } \ 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 4c2faae280..740eac38e1 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 @@ -58,6 +58,7 @@ internal fun ComposeScreen.createComposeView(context: Context): ComposeView { TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), hapticManager = uiDependencies.hapticManager, + snackbarHostState = uiDependencies.globalSnackbarHostState, ) { ScreenContent(modifier = screenModifier) } diff --git a/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml new file mode 100644 index 0000000000..c50e451c69 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml new file mode 100644 index 0000000000..a7b572f5f0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_storefront_24.xml b/core/ui/src/main/res/drawable/ic_storefront_24.xml new file mode 100644 index 0000000000..7f17e6c496 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_storefront_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index 80e03d9347..afa5e3bf04 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -11,5 +11,5 @@ interface ReduxStateHolder { suspend fun onUserWalletSelected(userWallet: UserWallet) - fun sendFeedbackEmail() + fun dispatchDialogShow(dialog: StateDialog) } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt similarity index 83% rename from core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt rename to domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index 75aa1f0579..24571182fb 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -1,4 +1,4 @@ -package com.tangem.core.navigation +package com.tangem.domain.redux interface StateDialog { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index fbd703a49a..93e60c90ef 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -4,16 +4,20 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.feedback.FeedbackType +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.details.routing.DetailsRoute import com.tangem.features.tester.api.TesterRouter import javax.inject.Inject +import kotlin.reflect.KClass // TODO: Remove after [REDACTED_JIRA] internal class DetailsRouter @Inject constructor( private val reduxNavController: ReduxNavController, - private val reduxStateHolder: ReduxStateHolder, + private val feedbackManager: FeedbackManager, private val testerRouter: TesterRouter, + private val urlOpener: UrlOpener, ) : Router { override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { @@ -23,13 +27,13 @@ internal class DetailsRouter @Inject constructor( reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params)) } is DetailsRoute.Feedback -> { - reduxStateHolder.sendFeedbackEmail() + feedbackManager.sendEmail(FeedbackType.Feedback) } is DetailsRoute.TesterMenu -> { testerRouter.startTesterScreen() } is DetailsRoute.Url -> { - reduxNavController.navigate(NavigationAction.OpenUrl(route.url)) + urlOpener.openUrl(route.url) } } onComplete(true) @@ -43,13 +47,15 @@ internal class DetailsRouter @Inject constructor( onComplete(true) } + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + TODO("This class will be removed in future") + } + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - if (route is DetailsRoute.Screen) { - reduxNavController.popBackStack(route.screen) - onComplete(true) - } else { - reduxNavController.getBackStack() - onComplete(false) - } + TODO("This class will be removed in future") + } + + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + TODO("This class will be removed in future") } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index 1f4026d32a..2aa9daebfe 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -2,10 +2,10 @@ package com.tangem.features.details.component import androidx.compose.material3.SnackbarHostState import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId -interface DetailsComponent : ComposableContentProvider { +interface DetailsComponent : ComposableContentComponent { val snackbarHostState: SnackbarHostState diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt index 8e7ffc063a..37791d71d0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -1,9 +1,9 @@ package com.tangem.features.details.component import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent -interface UserWalletListComponent : ComposableContentProvider { +interface UserWalletListComponent : ComposableContentComponent { interface Factory { fun create(context: AppComponentContext): UserWalletListComponent diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 3981734a85..d91ffd5ce1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -3,6 +3,7 @@ package com.tangem.features.details.component.preview import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -17,12 +18,12 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( - router = PreviewRouter(), + router = DummyRouter(), ).buldAll(isWalletConnectAvailable = true) } private val previewFooter = DetailsFooterUM( - socials = SocialsBuilder(PreviewRouter()).buildAll(), + socials = SocialsBuilder(DummyRouter()).buildAll(), appVersion = "1.0.0-preview", ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt deleted file mode 100644 index 49366db923..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.details.component.preview - -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router - -internal class PreviewRouter : Router { - override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } -} \ No newline at end of file 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 c0f66280bc..95413f3fc5 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,7 +18,7 @@ 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 com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.res.TangemTheme @@ -36,7 +36,7 @@ private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f internal fun DetailsScreen( state: DetailsUM, snackbarHostState: SnackbarHostState, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { val backgroundColor = TangemTheme.colors.background.secondary @@ -118,7 +118,7 @@ private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, mo @Composable private fun Content( state: DetailsUM, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { LazyColumn( @@ -152,7 +152,7 @@ private fun Content( @Composable private fun Block( model: DetailsItemUM, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { Column( 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 243c6849d7..cd539ea937 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 @@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference @@ -22,7 +21,6 @@ import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import javax.inject.Inject @ComponentScoped @@ -44,7 +42,7 @@ internal class UserWalletSaver @Inject constructor( saveWallet(userWallet) - router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) }, recover = { error -> val message = error.message @@ -77,7 +75,7 @@ internal class UserWalletSaver @Inject constructor( selectWalletUseCase(userWallet.walletId).bind() } - router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { diff --git a/features/disclaimer/api/.gitignore b/features/disclaimer/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts new file mode 100644 index 0000000000..fcd117da1b --- /dev/null +++ b/features/disclaimer/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt new file mode 100644 index 0000000000..5575d38b51 --- /dev/null +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.disclaimer.api + +import androidx.fragment.app.Fragment + +interface DisclaimerRouter { + + fun entryFragment(): Fragment + + companion object { + const val IS_TOS_ACCEPTED_KEY = "is_tos_accepted" + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/.gitignore b/features/disclaimer/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts new file mode 100644 index 0000000000..bc658e93f3 --- /dev/null +++ b/features/disclaimer/impl/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.card) + + /** Feature modules */ + implementation(projects.features.disclaimer.api) + + /** DI */ + implementation(deps.hilt.android) + implementation(project(":domain:settings")) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt new file mode 100644 index 0000000000..e0a30ce395 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -0,0 +1,58 @@ +package com.tangem.features.disclaimer.impl + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen +import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class DisclaimerFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + @Inject + lateinit var reduxNavController: ReduxNavController + + private val viewModel by viewModels() + + private val isTosAccepted: Boolean + get() = arguments?.getBoolean(DisclaimerRouter.IS_TOS_ACCEPTED_KEY) ?: false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.primary + BackHandler { + if (isTosAccepted) { + reduxNavController.popBackStack() + } else { + requireActivity().finish() + } + } + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + DisclaimerScreen(viewModel.state, reduxNavController::popBackStack) + } + + companion object { + /** Create disclaimer fragment instance */ + fun create(): DisclaimerFragment = DisclaimerFragment() + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt new file mode 100644 index 0000000000..8b3d34e029 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [DisclaimerRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +object DisclaimerRouterModule { + + @Provides + @ActivityScoped + fun provideDisclaimerRouter(reduxNavController: ReduxNavController): DisclaimerRouter { + return DefaultDisclaimerRouter(reduxNavController) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt new file mode 100644 index 0000000000..4860d3265d --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt @@ -0,0 +1,23 @@ +package com.tangem.features.disclaimer.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.impl.DisclaimerFragment +import javax.inject.Inject + +internal class DefaultDisclaimerRouter @Inject constructor( + private val reduxNavController: ReduxNavController, +) : InnerDisclaimerRouter { + + override fun entryFragment(): Fragment = DisclaimerFragment.create() + + override fun openPushNotificationPermission() { + // todo [REDACTED_JIRA] + } + + override fun openHome() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt new file mode 100644 index 0000000000..5077db0901 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt @@ -0,0 +1,10 @@ +package com.tangem.features.disclaimer.impl.navigation + +import com.tangem.features.disclaimer.api.DisclaimerRouter + +internal interface InnerDisclaimerRouter : DisclaimerRouter { + + fun openPushNotificationPermission() + + fun openHome() +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt new file mode 100644 index 0000000000..01d0a398a6 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt @@ -0,0 +1,16 @@ +package com.tangem.features.disclaimer.impl.presentation.state + +internal data class DisclaimerState( + val url: String, + val isTosAccepted: Boolean, + val onAccept: (Boolean) -> Unit, +) + +internal object DummyDisclaimer { + + val state = DisclaimerState( + url = "https://tangem.com/tangem_tos.html", + isTosAccepted = false, + onAccept = {}, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt new file mode 100644 index 0000000000..f99bd7bc4e --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -0,0 +1,174 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.Manifest +import android.content.res.Configuration +import android.os.Build +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.disclaimer.impl.R +import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState +import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer + +@Composable +internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { + val bottomPadding = if (state.isTosAccepted) TangemTheme.dimens.spacing16 else TangemTheme.dimens.size64 + Box( + modifier = Modifier + .statusBarsPadding() + .background(TangemColorPalette.Dark6), + ) { + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = bottomPadding), + ) { + AppBarWithAdditionalButtons( + text = resourceReference(R.string.disclaimer_title), + startButton = AdditionalButton( + iconRes = R.drawable.ic_back_24, + onIconClicked = onBackClick, + ).takeIf { state.isTosAccepted }, + ) + DisclaimerContent(state.url) + } + if (!state.isTosAccepted) { + DisclaimerButton(state.onAccept) + } + } +} + +@Composable +private fun ColumnScope.DisclaimerContent(url: String) { + val transparent = Color.Transparent + + val progressState = remember { mutableStateOf(ProgressState.Loading) } + val webClient = remember { DisclaimerWebViewClient(progressState) } + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + AndroidView( + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(transparent.toArgb()) + settings.allowFileAccess = false + settings.javaScriptEnabled = false + overScrollMode = View.OVER_SCROLL_NEVER + webViewClient = webClient + loadUrl(url) + } + }, + ) + + when (progressState.value) { + ProgressState.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) + ProgressState.Error -> Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing16) + .align(Alignment.Center), + ) { + Text( + text = stringResource(id = R.string.disclaimer_error_loading), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.constantWhite, + ) + PrimaryButton( + text = stringResource(R.string.common_retry), + onClick = { webClient.reset() }, + ) + } + else -> Unit + } + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { + val shouldAskPushPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS).status.isGranted + } else { + true + } + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size110 + bottomInsetsPx.dp) + .background( + Brush.verticalGradient( + colors = listOf( + TangemTheme.colors.background.primary.copy(alpha = 0f), + TangemTheme.colors.background.primary.copy(alpha = 0.75f), + TangemTheme.colors.background.primary.copy(alpha = 0.95f), + TangemTheme.colors.background.primary, + ), + ), + ), + ) + PrimaryButton( + text = stringResource(id = R.string.common_accept), + onClick = { onAccept(shouldAskPushPermission) }, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DisclaimerScreen_Preview() { + TangemThemePreview { + DisclaimerScreen(state = DummyDisclaimer.state, onBackClick = {}) + } +} +// endregion \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt new file mode 100644 index 0000000000..348cad4107 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt @@ -0,0 +1,62 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.graphics.Bitmap +import android.webkit.* +import androidx.compose.runtime.MutableState + +internal enum class ProgressState { + Loading, + Done, + Error, +} + +internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { + + private var loadingUrl: String? = null + private var loadedUrl: String? = null + + fun reset() { + loadingUrl = null + loadedUrl = null + progressState.value = ProgressState.Loading + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + if (loadingUrl != url) progressState.value = ProgressState.Loading + loadingUrl = url + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + if (loadedUrl != url) progressState.value = ProgressState.Done + loadedUrl = url + } + + override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, resourceRequest, error) + error?.let { progressState.value = ProgressState.Error } + } + + override fun onReceivedHttpError( + view: WebView?, + resourceRequest: WebResourceRequest?, + errorResponse: WebResourceResponse?, + ) { + super.onReceivedHttpError(view, resourceRequest, errorResponse) + + if (resourceRequest != null && errorResponse != null) { + val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl + val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE + val isNotDone = progressState.value != ProgressState.Done + if (isDifferentUrl || isSuccessCode || isNotDone) return + progressState.value = ProgressState.Error + } + } + + companion object { + private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt new file mode 100644 index 0000000000..d80a1236ed --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt @@ -0,0 +1,6 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +internal interface DisclaimerClickIntents { + + fun onAccept(shouldAskPushPermission: Boolean) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt new file mode 100644 index 0000000000..bfc97b16d8 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt @@ -0,0 +1,42 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.domain.card.repository.CardRepository +import com.tangem.features.disclaimer.api.DisclaimerRouter.Companion.IS_TOS_ACCEPTED_KEY +import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter +import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class DisclaimerViewModel @Inject constructor( + private val cardRepository: CardRepository, + private val disclaimerRouter: DefaultDisclaimerRouter, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, DisclaimerClickIntents { + + private val isTosAccepted: Boolean = savedStateHandle[IS_TOS_ACCEPTED_KEY] ?: false + + val state: DisclaimerState + get() = DisclaimerState( + onAccept = ::onAccept, + url = DISCLAIMER_URL, + isTosAccepted = isTosAccepted, + ) + + override fun onAccept(shouldAskPushPermission: Boolean) { + viewModelScope.launch { + cardRepository.acceptTangemTOS() + // todo [REDACTED_JIRA] + disclaimerRouter.openHome() + } + } + + private companion object { + const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" + } +} \ No newline at end of file diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 6629611e63..9f081a463f 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) implementation(deps.compose.ui) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 7f38cacd35..352d2aaf2b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -18,7 +19,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { - return DefaultSendRouter(reduxNavController) + fun provideSendRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): SendRouter { + return DefaultSendRouter(reduxNavController, urlOpener) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 0629979fd1..59eade45d0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -14,12 +15,13 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultSendRouter( private val reduxNavController: ReduxNavController, + private val urlOpener: UrlOpener, ) : InnerSendRouter { override fun getEntryFragment(): Fragment = SendFragment.create() override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index e96be559de..9648e0164d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel @@ -31,6 +32,9 @@ internal class StakingFragment : ComposeFragment() { @Inject lateinit var router: StakingRouter + @Inject + lateinit var stateController: StakingStateController + @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -48,6 +52,7 @@ internal class StakingFragment : ComposeFragment() { innerStakingRouter, StakingStateRouter( fragmentManager = WeakReference(parentFragmentManager), + stateController = stateController, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 9fdd84f082..fc51c5f7e7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -35,7 +35,7 @@ internal class StakingStateController @Inject constructor() { return StakingUiState( clickIntents = StakingClickIntentsStub, cryptoCurrencyName = "", - currentScreen = StakingUiStateType.InitialInfo, + currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = StakingStates.AmountState.Empty(), validatorState = StakingStates.ValidatorState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 8e7182f088..bc00ead8ca 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,22 +1,15 @@ package com.tangem.features.staking.impl.presentation.state import androidx.fragment.app.FragmentManager -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.lang.ref.WeakReference internal class StakingStateRouter( private val fragmentManager: WeakReference, + private val stateController: StakingStateController, ) { - private var mutableCurrentState: MutableStateFlow = MutableStateFlow(getInitState()) - - val currentState: StateFlow - get() = mutableCurrentState.asStateFlow() - fun clear() { - mutableCurrentState.update { getInitState() } + stateController.update { it.copy(currentStep = getInitialState()) } } fun popBackStack() { @@ -24,49 +17,48 @@ internal class StakingStateRouter( } fun onBackClick(isSuccess: Boolean = false) { - val type = currentState.value + val type = stateController.uiState.value.currentStep when { isSuccess -> popBackStack() else -> when (type) { - StakingUiStateType.Amount -> showInitial() - StakingUiStateType.ValidatorAndFee -> showAmount() + StakingStep.Amount -> showInitial() + StakingStep.ValidatorAndFee -> showAmount() else -> popBackStack() } } } fun onNextClick() { - when (currentState.value) { - StakingUiStateType.InitialInfo -> showAmount() - StakingUiStateType.Amount, - StakingUiStateType.ValidatorAndFee, - -> showConfirm() - StakingUiStateType.Confirm -> onBackClick() + when (stateController.uiState.value.currentStep) { + StakingStep.InitialInfo -> showAmount() + StakingStep.Amount -> showValidator() + StakingStep.ValidatorAndFee -> showConfirm() + StakingStep.Confirm -> onBackClick() } } fun onPrevClick() { - when (currentState.value) { - StakingUiStateType.Amount -> showInitial() + when (stateController.uiState.value.currentStep) { + StakingStep.Amount -> showInitial() else -> popBackStack() } } private fun showInitial() { - mutableCurrentState.update { StakingUiStateType.InitialInfo } + stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } fun showAmount() { - mutableCurrentState.update { StakingUiStateType.Amount } + stateController.update { it.copy(currentStep = StakingStep.Amount) } } fun showValidator() { - mutableCurrentState.update { StakingUiStateType.ValidatorAndFee } + stateController.update { it.copy(currentStep = StakingStep.ValidatorAndFee) } } fun showConfirm() { - mutableCurrentState.update { StakingUiStateType.Confirm } + stateController.update { it.copy(currentStep = StakingStep.Confirm) } } - private fun getInitState() = StakingUiStateType.InitialInfo + private fun getInitialState() = StakingStep.InitialInfo } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 3852cd6ed2..1253b63881 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -12,7 +12,7 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten internal data class StakingUiState( val clickIntents: StakingClickIntents, val cryptoCurrencyName: String, - val currentScreen: StakingUiStateType, + val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: StakingStates.AmountState, val validatorState: StakingStates.ValidatorState, @@ -94,6 +94,8 @@ internal sealed class StakingStates { sealed class ConfirmStakingState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val isSuccess: Boolean, + val isStaking: Boolean, ) : ConfirmStakingState() data class Empty( @@ -102,7 +104,7 @@ internal sealed class StakingStates { } } -enum class StakingUiStateType { +enum class StakingStep { InitialInfo, Amount, ValidatorAndFee, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 16a8763435..b09e5d9f4c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -5,4 +5,8 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} + + override fun onNextClick() {} + + override fun onPrevClick() {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 3ee1b970cd..e16098575b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import java.math.BigDecimal @@ -24,7 +24,7 @@ internal class SetInitialDataStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( clickIntents = clickIntents, - currentScreen = StakingUiStateType.InitialInfo, + currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt new file mode 100644 index 0000000000..e1324855c8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.runtime.Composable +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingAmountContent(state: StakingStates.AmountState) { + if (state !is StakingStates.AmountState.Data) return + + // TODO staking +} \ No newline at end of file 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 7ff32c89e2..97491c4a36 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 @@ -3,9 +3,11 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.foundation.verticalScroll +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 @@ -15,7 +17,6 @@ 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.components.PrimaryButton import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.extensions.resolveReference @@ -32,19 +33,13 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { Column( modifier = Modifier // Do not put fillMaxSize() in here .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), ) { MetricsBlock(state) Spacer(modifier = Modifier.height(8.dp)) StakingDetailsRows(state) Spacer(modifier = Modifier.height(8.dp)) - PrimaryButton( - text = stringResource(id = R.string.common_stake), - modifier = Modifier.fillMaxWidth(), - onClick = { - // TODO staking - }, - ) } } @@ -185,7 +180,7 @@ private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvide override val values: Sequence get() = sequenceOf( StakingStates.InitialInfoState.Data( - isPrimaryButtonEnabled = false, + isPrimaryButtonEnabled = true, available = "15 SOL", onStake = "0 SOL", aprRange = stringReference("2.54-5.12%"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt new file mode 100644 index 0000000000..aaceadf9c6 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -0,0 +1,119 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +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.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingStep + +@Composable +internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + StakingNavigationButton( + uiState = uiState, + modifier = Modifier, + ) + } +} + +@Composable +private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + val confirmState = uiState.confirmStakingState + val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false + val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false + + val isButtonsVisible = uiState.currentStep != StakingStep.Confirm + val isStakingState = uiState.currentStep == StakingStep.Confirm && !isSuccess && !isStaking + + val (buttonTextId, buttonClick) = getButtonData( + currentState = uiState, + ) + val isButtonEnabled = isButtonEnabled(uiState) + val buttonIcon = if (isStakingState) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + } + + Row(modifier = modifier) { + AnimatedVisibility( + visible = isButtonsVisible, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + Row { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + SpacerW12() + } + } + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled, + onClick = { + if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + buttonClick() + }, + showProgress = false, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } +} + +private fun getButtonData(currentState: StakingUiState): Pair Unit> { + return when (currentState.currentStep) { + StakingStep.InitialInfo, + StakingStep.Amount, + StakingStep.ValidatorAndFee, + StakingStep.Confirm, + -> R.string.common_next to { currentState.clickIntents.onNextClick() } + } +} + +private fun isButtonEnabled(uiState: StakingUiState): Boolean { + return when (uiState.currentStep) { + StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled + StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled + StakingStep.ValidatorAndFee -> uiState.validatorState.isPrimaryButtonEnabled + StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index c1f1bf5a95..6a0d321903 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.state.StakingStep import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -40,25 +40,28 @@ internal fun StakingScreen(uiState: StakingUiState) { uiState = uiState, modifier = Modifier.weight(1f), ) + StakingNavigationButtons( + uiState = uiState, + ) } } @Composable private fun SendAppBar(uiState: StakingUiState) { - val titleRes = when (uiState.currentScreen) { - StakingUiStateType.InitialInfo -> stringResource(id = R.string.common_stake) - StakingUiStateType.Amount -> stringResource(id = R.string.send_amount_label) - StakingUiStateType.ValidatorAndFee -> stringResource(id = R.string.common_stake) - StakingUiStateType.Confirm -> "" + val titleRes = when (uiState.currentStep) { + StakingStep.InitialInfo -> stringResource(id = R.string.common_stake) + StakingStep.Amount -> stringResource(id = R.string.send_amount_label) + StakingStep.ValidatorAndFee -> stringResource(id = R.string.common_stake) + StakingStep.Confirm -> "" } - val backIcon = when (uiState.currentScreen) { - StakingUiStateType.Amount, - StakingUiStateType.ValidatorAndFee, - StakingUiStateType.Confirm, + val backIcon = when (uiState.currentStep) { + StakingStep.Amount, + StakingStep.ValidatorAndFee, + StakingStep.Confirm, -> { R.drawable.ic_close_24 } - StakingUiStateType.InitialInfo -> { + StakingStep.InitialInfo -> { R.drawable.ic_back_24 } } @@ -74,7 +77,7 @@ private fun SendAppBar(uiState: StakingUiState) { @OptIn(ExperimentalAnimationApi::class) @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { - val currentScreen = uiState.currentScreen + val currentScreen = uiState.currentStep var currentStateProxy by remember { mutableStateOf(currentScreen) } var isTransitionAnimationRunning by remember { mutableStateOf(false) } @@ -115,10 +118,16 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M isTransitionAnimationRunning = transition.targetState != transition.currentState when (state) { - StakingUiStateType.InitialInfo -> StakingInitialInfoContent( + StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, ) - else -> Unit + StakingStep.Amount -> StakingAmountContent( + state = uiState.amountState, + ) + StakingStep.ValidatorAndFee -> StakingValidatorAndFeeContent( + state = uiState.validatorState, + ) + else -> TODO() } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt new file mode 100644 index 0000000000..a73d0000d4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.runtime.Composable +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingValidatorAndFeeContent(state: StakingStates.ValidatorState) { + if (state !is StakingStates.ValidatorState.Data) return + + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 30f890ae97..8176c2ea6c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -3,4 +3,8 @@ package com.tangem.features.staking.impl.presentation.viewmodel internal interface StakingClickIntents { fun onBackClick() + + fun onNextClick() + + fun onPrevClick() } \ No newline at end of file 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 2c94bdea46..22c0686b9c 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 @@ -63,6 +63,14 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } + override fun onNextClick() { + stakingStateRouter.onNextClick() + } + + override fun onPrevClick() { + stakingStateRouter.onBackClick() + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt deleted file mode 100644 index a64be25542..0000000000 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.tester.api - -/** - * Interface for app restarter - */ -interface AppRestarter { - - fun restart() -} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 86db03dca7..01480f6da6 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Feature Apis */ implementation(projects.features.tester.api) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt deleted file mode 100644 index 22d2d4d3a9..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.tester - -import android.app.Activity - -/** - * Wraps the main activity class to avoid type erasure issues during injection. - * - * @property clazz activity class - */ -class ActivityClassWrapper( - val clazz: Class, -) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt deleted file mode 100644 index 34cd927e71..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.tester.apprestarter - -import android.app.Activity -import android.content.Context -import android.content.Intent -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.features.tester.api.AppRestarter - -/** - * Entity that kills the process and restarts the main activity - * @property context Activity context - */ -internal class DefaultAppRestarter( - private val context: Context, - private val activityClassWrapper: ActivityClassWrapper, -) : AppRestarter { - - override fun restart() { - if (context !is Activity) return - - context.finish() - context.startActivity( - Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP }, - ) - Runtime.getRuntime().exit(0) - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt deleted file mode 100644 index 4891a60f12..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.feature.tester.di - -import android.content.Context -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.feature.tester.apprestarter.DefaultAppRestarter -import com.tangem.features.tester.api.AppRestarter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped - -@Module -@InstallIn(ActivityComponent::class) -internal object RestarterModule { - - @Provides - @ActivityScoped - fun provideAppRestarter( - @ActivityContext context: Context, - activityClassWrapper: ActivityClassWrapper, - ): AppRestarter { - return DefaultAppRestarter(context, activityClassWrapper) - } -} \ 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 ef7ab21998..c94208933f 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 @@ -6,6 +6,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -18,7 +19,6 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState 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.features.tester.api.AppRestarter import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -35,7 +35,7 @@ internal class TesterActivity : ComposeActivity() { lateinit var testerRouter: TesterRouter @Inject - lateinit var appRestarter: AppRestarter + lateinit var appFinisher: AppFinisher private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { @@ -70,7 +70,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.FEATURE_TOGGLES.name) { val viewModel = hiltViewModel().apply { - setupInteractions(innerTesterRouter, appRestarter) + setupInteractions(innerTesterRouter, appFinisher) } FeatureTogglesScreen(state = viewModel.uiState) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index d3459ac31f..2d810e1ebf 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -7,10 +7,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter -import com.tangem.features.tester.api.AppRestarter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -40,10 +40,10 @@ internal class FeatureTogglesViewModel @Inject constructor( } /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ - fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) { + fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) { uiState = uiState.copy( onBackClick = router::back, - onApplyChangesClick = appRestarter::restart, + onApplyChangesClick = appFinisher::restart, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt index b87eb5b538..186a4fcc7b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -15,7 +17,11 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(reduxNavController: ReduxNavController): TokenDetailsRouter { - return DefaultTokenDetailsRouter(reduxNavController) + fun provideTokenDetailsRouter( + reduxNavController: ReduxNavController, + urlOpener: UrlOpener, + shareManager: ShareManager, + ): TokenDetailsRouter { + return DefaultTokenDetailsRouter(reduxNavController, urlOpener, shareManager) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index cadf3b9549..16b2a547ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -5,6 +5,8 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment @@ -12,6 +14,8 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultTokenDetailsRouter( private val reduxNavController: ReduxNavController, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, ) : InnerTokenDetailsRouter { override fun getEntryFragment(): Fragment = TokenDetailsFragment() @@ -21,11 +25,11 @@ internal class DefaultTokenDetailsRouter( } override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun share(text: String) { - reduxNavController.navigate(NavigationAction.Share(text)) + shareManager.shareText(text) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..536e255a98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module @@ -15,7 +17,11 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter( + reduxNavController: ReduxNavController, + reduxStateHolder: ReduxStateHolder, + urlOpener: UrlOpener, + ): WalletRouter { + return DefaultWalletRouter(reduxNavController, reduxStateHolder, urlOpener) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 6276cdb2e9..e30b525b92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -19,7 +19,9 @@ import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.navigation.StateDialog +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.onboarding.navigation.OnboardingRouter @@ -37,6 +39,8 @@ import kotlin.properties.Delegates /** Default implementation of wallet feature router */ internal class DefaultWalletRouter( private val reduxNavController: ReduxNavController, + private val reduxStateHolder: ReduxStateHolder, + private val urlOpener: UrlOpener, ) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() @@ -138,7 +142,7 @@ internal class DefaultWalletRouter( } override fun openUrl(url: String) { - reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { @@ -171,11 +175,7 @@ internal class DefaultWalletRouter( } override fun openScanFailedDialog() { - reduxNavController.navigate( - action = NavigationAction.OpenDialog( - StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN), - ), - ) + reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN)) } private companion object { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index c4f0245a43..b549e9bf7b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -91,7 +91,7 @@ tangemBlockchainSdk = "develop-654" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-360" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.21-tangem13" +tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -168,6 +168,7 @@ compose-navigation-hilt = { module = "androidx.hilt:hilt-navigation-compose", ve compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "compose-accompanist" } compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" } compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" } +compose-accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "compose-accompanist" } compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" } compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "compose-reorderable" } # endregion Compose diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e8f4f4945..914e2ac058 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,11 +35,11 @@ dependencyResolutionManagement { mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeGroupAndSubgroups("com.tangem.vico") includeModule("com.tangem", "blstlib") includeModule("com.tangem", "blockchain") includeModule("com.tangem", "wallet-core-proto") includeModule("com.tangem", "wallet-core") - includeModule("com.tangem", "vico") } } maven { @@ -176,6 +176,9 @@ include(":features:staking:impl") include(":features:details:api") include(":features:details:impl") + +include(":features:disclaimer:api") +include(":features:disclaimer:impl") // endregion Feature modules // region Domain modules