Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-14 13:36:51 +04:00
commit 0d0cb3e871
77 changed files with 542 additions and 282 deletions

View file

@ -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<Intent>?
}
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)
}

View file

@ -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
}

View file

@ -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<C : ComposableContentComponent, P : Any, F : ComponentFactory<C, P>>(
private val contextProvider: Provider<AppComponentContext>,
private val params: P,
private val componentFactory: F,
) {
fun build(): C = componentFactory.create(contextProvider(), params)
}
companion object {
private var componentBuilder: ComponentBuilder<*, *, *>? = null
fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<C, P>> newInstance(
contextProvider: Provider<AppComponentContext>,
params: P,
componentFactory: F,
): Fragment {
this@Companion.componentBuilder = ComponentBuilder(contextProvider, params, componentFactory)
return DecomposeFragment()
}
}
}

View file

@ -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() {

View file

@ -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),

View file

@ -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.*

View file

@ -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<AppState>.dispatchOpenUrl(url: String) {
inject(DaggerGraphState::urlOpener).openUrl(url)
}
fun Store<*>.dispatchShare(url: String) {
dispatch(NavigationAction.Share(url))
fun Store<AppState>.dispatchShare(url: String) {
inject(DaggerGraphState::shareManager).shareText(url)
}
inline fun <reified T> Store<AppState>.inject(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -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)
}

View file

@ -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"

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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,

View file

@ -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<AppState> = { _, 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

View file

@ -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)
}
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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,
)

View file

@ -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()
}
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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<SocialNetworkLink> {

View file

@ -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]

View file

@ -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
/**

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -0,0 +1,8 @@
package com.tangem.core.decompose.factory
import com.tangem.core.decompose.context.AppComponentContext
interface ComponentFactory<Component : Any, Params : Any> {
fun create(context: AppComponentContext, params: Params): Component
}

View file

@ -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<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.popWhile(
predicate = { it::class != routeClass },
onComplete = onComplete,
)
}
}

View file

@ -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<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
}

View file

@ -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<out Route>, onComplete: (isSuccess: Boolean) -> Unit = {})
}

View file

@ -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 <reified R : Route> Router.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) {
popTo(R::class, onComplete)
}

View file

@ -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<AppCompatActivity>) : NavigationAction()
data class ActivityDestroyed(val activity: WeakReference<AppCompatActivity>) : NavigationAction()

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.feedback
class DummyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackType) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.feedback
interface FeedbackManager {
fun sendEmail(type: FeedbackType)
}

View file

@ -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()
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.finisher
interface AppFinisher {
fun finish()
fun restart()
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.share
class DummyShareManager : ShareManager {
override fun shareText(text: String) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.share
interface ShareManager {
fun shareText(text: String)
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.url
class DummyUrlOpener : UrlOpener {
override fun openUrl(url: String) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.url
interface UrlOpener {
fun openUrl(url: String)
}

View file

@ -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

View file

@ -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
}

View file

@ -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<StateEvent<EventMessage>> = MutableStateFlow(consumedEvent()),
) : UiMessageHandler, StateFlow<StateEvent<EventMessage>> by events {

View file

@ -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<HapticManager> {
error("No HapticManager provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}

View file

@ -58,6 +58,7 @@ internal fun ComposeScreen.createComposeView(context: Context): ComposeView {
TangemTheme(
isDark = shouldUseDarkTheme(appThemeMode),
hapticManager = uiDependencies.hapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
) {
ScreenContent(modifier = screenModifier)
}

View file

@ -11,5 +11,5 @@ interface ReduxStateHolder {
suspend fun onUserWalletSelected(userWallet: UserWallet)
fun sendFeedbackEmail()
fun dispatchDialogShow(dialog: StateDialog)
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.navigation
package com.tangem.domain.redux
interface StateDialog {

View file

@ -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<out Route>, 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")
}
}

View file

@ -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

View file

@ -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

View file

@ -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",
)

View file

@ -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 */
}
}

View file

@ -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(

View file

@ -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<Error>.createUserWallet(response: ScanResponse): UserWallet {

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -1,9 +0,0 @@
package com.tangem.features.tester.api
/**
* Interface for app restarter
*/
interface AppRestarter {
fun restart()
}

View file

@ -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)

View file

@ -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<out Activity>,
)

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<FeatureTogglesViewModel>().apply {
setupInteractions(innerTesterRouter, appRestarter)
setupInteractions(innerTesterRouter, appFinisher)
}
FeatureTogglesScreen(state = viewModel.uiState)

View file

@ -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,
)
}

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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 {