diff --git a/app/src/main/java/com/tangem/tap/common/Handler.kt b/app/src/main/java/com/tangem/tap/common/Handler.kt deleted file mode 100644 index 7842da9fbf..0000000000 --- a/app/src/main/java/com/tangem/tap/common/Handler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.common - -import android.os.Handler -import android.os.HandlerThread -import android.os.Looper - -private val uiHandler = Handler(Looper.getMainLooper()) -private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper) - -fun postUi(ms: Long = 0, func: Runnable) { - if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms) -} - -fun postUiDelayBg(ms: Long, func: Runnable) { - backgroundHandler.postDelayed({ uiHandler.post(func) }, ms) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt index 392f340405..7a5093628b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt @@ -11,7 +11,7 @@ class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSo ), ) { enum class Buttons(val event: String) { - TRY_AGAIN("Try again button"), HOW_TO_SCAN("Button blog"), + TRY_AGAIN("Button try again"), } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt b/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt deleted file mode 100644 index 61adc5cd45..0000000000 --- a/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.tap.common.entities - -import com.tangem.tap.common.toggleWidget.WidgetState - -enum class ProgressState : WidgetState { Loading, Done, Error } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt deleted file mode 100644 index 214138e5dc..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.common.extensions - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.transition.AutoTransition -import androidx.transition.Transition -import androidx.transition.TransitionManager - -/** -[REDACTED_AUTHOR] - */ -fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View { - return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot) -} - -fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) { - TransitionManager.beginDelayedTransition(this, transition) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt deleted file mode 100644 index 10b6a1f7a6..0000000000 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.common.toggleWidget - -import android.view.View - -/** -[REDACTED_AUTHOR] - */ -interface WidgetState - -interface ViewStateWidget { - val mainView: View - fun changeState(state: WidgetState) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt b/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt deleted file mode 100644 index e250846a42..0000000000 --- a/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.transitions - -import androidx.transition.ChangeBounds -import androidx.transition.ChangeTransform -import androidx.transition.Fade -import androidx.transition.TransitionSet - -class HomeToOnboardingTransition : TransitionSet() { - init { - ordering = ORDERING_TOGETHER - addTransition(Fade()) - addTransition(ChangeTransform()) - addTransition(ChangeBounds()) - } -} - -class InternalNoteLayoutTransition : TransitionSet() { - init { - ordering = ORDERING_TOGETHER - addTransition(ChangeTransform()) - addTransition(ChangeBounds()) - addTransition(Fade()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt new file mode 100644 index 0000000000..ffb2163692 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.features.scanfails + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.tap.features.scanfails.ui.ScanFailsDialogContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class ScanFailsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : AppComponentContext by appComponentContext, ComposableContentComponent, ScanFailsRequester { + + private val model: ScanFailsModel = getOrCreateModel(params) + + override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + model.show(source) + return model.waitResult() + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + if (state.isShown) { + ScanFailsDialogContent(state = state) + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): ScanFailsComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt new file mode 100644 index 0000000000..e94e714d65 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt @@ -0,0 +1,84 @@ +package com.tangem.tap.features.scanfails + +import com.tangem.common.TangemBlogUrlBuilder +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics +import com.tangem.tap.features.scanfails.ui.ScanFailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class ScanFailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + private val result = MutableStateFlow(null) + + val uiState: StateFlow + field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss)) + + fun show(source: ScanFailsRequester.Source) { + val analyticsSource = source.toAnalyticsSource() + result.value = null + uiState.update { + ScanFailsUM( + isShown = true, + onHowToScan = { onHowToScan(analyticsSource) }, + onRequestSupport = { onRequestSupport(analyticsSource) }, + onDismiss = ::dismiss, + ) + } + } + + suspend fun waitResult(): ScanFailsRequester.Result { + return result.filterNotNull().first().also { result.value = null } + } + + fun dismiss() { + result.value = ScanFailsRequester.Result.Dismissed + uiState.update { it.copy(isShown = false) } + } + + private fun onHowToScan(source: AnalyticsParam.ScreensSources) { + analyticsEventHandler.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN, + source = source, + ), + ) + modelScope.launch { + urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.HowToScan)) + } + } + + private fun onRequestSupport(source: AnalyticsParam.ScreensSources) { + analyticsEventHandler.send(Basic.ButtonSupport(source)) + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem) + } + } + + private fun ScanFailsRequester.Source.toAnalyticsSource(): AnalyticsParam.ScreensSources = when (this) { + ScanFailsRequester.Source.MAIN -> AnalyticsParam.ScreensSources.Main + ScanFailsRequester.Source.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn + ScanFailsRequester.Source.SETTINGS -> AnalyticsParam.ScreensSources.Settings + ScanFailsRequester.Source.INTRO -> AnalyticsParam.ScreensSources.Intro + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt new file mode 100644 index 0000000000..806d511714 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.scanfails + +import com.tangem.domain.card.ScanFailsRequester +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ScanFailsRequesterProxy @Inject constructor() : ScanFailsRequester { + + val componentRequester = MutableStateFlow(null) + + override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + return withTimeout(timeMillis = 1000) { + componentRequester.filterNotNull().first() + }.show(source) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt new file mode 100644 index 0000000000..6d637e7594 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.features.scanfails.di + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.model.Model +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.tap.features.scanfails.ScanFailsComponent +import com.tangem.tap.features.scanfails.ScanFailsModel +import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ScanFailsModule { + + @Binds + fun bindComponentFactory(impl: ScanFailsComponent.Factory): ComponentFactory + + @Binds + @IntoMap + @ClassKey(ScanFailsModel::class) + fun bindModel(model: ScanFailsModel): Model + + @Binds + @Singleton + fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt new file mode 100644 index 0000000000..fea911a528 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt @@ -0,0 +1,109 @@ +package com.tangem.tap.features.scanfails.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.tangem.core.ui.extensions.LocalUserInteractionTracker +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.trackUserInteraction +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun ScanFailsDialogContent(state: ScanFailsUM) { + val userInteractionTracker = LocalUserInteractionTracker.current + + Dialog( + onDismissRequest = state.onDismiss, + properties = DialogProperties(dismissOnClickOutside = true), + ) { + Column( + modifier = Modifier + .trackUserInteraction(userInteractionTracker) + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(16.dp), + ) + .padding(vertical = 16.dp, horizontal = 38.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Text( + text = stringResourceSafe(R.string.common_warning), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + textAlign = TextAlign.Center, + ) + + Text( + text = stringResourceSafe(R.string.alert_troubleshooting_scan_card_title), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.alert_button_how_to_scan), + onClick = state.onHowToScan, + color = TangemTheme.colors.text.accent, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.alert_button_request_support), + onClick = state.onRequestSupport, + color = TangemTheme.colors.text.accent, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.common_cancel), + onClick = state.onDismiss, + color = TangemTheme.colors.text.warning, + ) + } + } +} + +@Composable +private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color) { + Text( + text = text.uppercase(), + color = color, + style = TangemTheme.typography.button, + textAlign = TextAlign.Center, + modifier = Modifier.clickable(onClick = onClick), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 360, fontScale = 2f) +@Composable +private fun ScanFailsDialogContentPreview() { + TangemThemePreview { + ScanFailsDialogContent( + state = ScanFailsUM( + isShown = true, + onHowToScan = {}, + onRequestSupport = {}, + onDismiss = {}, + ), + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt new file mode 100644 index 0000000000..6e6ebebb8f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.scanfails.ui + +internal data class ScanFailsUM( + val isShown: Boolean = false, + val onHowToScan: () -> Unit = {}, + val onRequestSupport: () -> Unit = {}, + val onDismiss: () -> Unit = {}, +) \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 1ba2efe988..7f1d0b8d98 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -35,5 +35,9 @@ object TangemBlogUrlBuilder { data object WhatIsTransactionFee : Post { override val path: String = "what-is-a-transaction-fee-and-why-do-we-need-it" } + + data object HowToScan : Post { + override val path: String = "scan-tangem-card" + } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt new file mode 100644 index 0000000000..19fbb925c4 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.card + +interface ScanFailsRequester { + + suspend fun show(source: Source): Result + + enum class Source { MAIN, SIGN_IN, SETTINGS, INTRO } + + sealed class Result { + data object Dismissed : Result() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bd9c1ce7d9..26e9825ea2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -1,11 +1,7 @@ package com.tangem.features.hotwallet.accesscoderequest.ui -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -26,9 +22,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme @@ -42,12 +36,11 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val userInteractionTracker = LocalUserInteractionTracker.current Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .trackUserInteraction(userInteractionTracker), ) { AnimatedVisibility( - modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(),