Updated on 2026-08-14
This commit is contained in:
parent
9b1c90cf12
commit
2f7bcb9cbb
15 changed files with 316 additions and 88 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.tap.common.entities
|
||||
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
|
||||
enum class ProgressState : WidgetState { Loading, Done, Error }
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit, ScanFailsComponent> {
|
||||
override fun create(context: AppComponentContext, params: Unit): ScanFailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ScanFailsRequester.Result?>(null)
|
||||
|
||||
val uiState: StateFlow<ScanFailsUM>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ScanFailsRequester?>(null)
|
||||
|
||||
override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result {
|
||||
return withTimeout(timeMillis = 1000) {
|
||||
componentRequester.filterNotNull().first()
|
||||
}.show(source)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit, ScanFailsComponent>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ScanFailsModel::class)
|
||||
fun bindModel(model: ScanFailsModel): Model
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue