Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-20 13:32:25 +03:00
parent 886dc6ee32
commit 4697ca6584
21 changed files with 323 additions and 412 deletions

View file

@ -59,7 +59,6 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
@ -142,9 +141,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var qrScanningRouter: QrScanningRouter
@Inject
lateinit var deepLinksRegistry: DeepLinksRegistry

View file

@ -2,7 +2,7 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
@ -64,8 +64,8 @@ internal class ChildFactory @Inject constructor(
private val homeComponentFactory: HomeComponent.Factory,
private val tokenDetailsComponentFactory: TokenDetailsComponent.Factory,
private val walletConnectComponentFactory: WalletConnectComponent.Factory,
private val qrScanningComponentFactory: QrScanningComponent.Factory,
private val walletRouter: WalletRouter,
private val qrScanningRouter: QrScanningRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
@ -275,6 +275,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletConnectComponentFactory,
)
}
is AppRoute.QrScanning -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
is AppRoute.AppCurrencySelector,
is AppRoute.SaveWallet,
@ -285,7 +295,6 @@ internal class ChildFactory @Inject constructor(
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
is AppRoute.QrScanning,
is AppRoute.ReferralProgram,
is AppRoute.ResetToFactory,
is AppRoute.Wallet,
@ -383,7 +392,14 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { OnboardingWalletFragment() })
}
is AppRoute.QrScanning -> {
route.asFragmentChild(Provider { qrScanningRouter.getEntryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
)
}
is AppRoute.ReferralProgram -> {
route.asFragmentChild(Provider { ReferralFragment() })

View file

@ -93,20 +93,7 @@ sealed class AppRoute(val path: String) : Route {
"&$amount" +
"&$tag" +
"&$destinationAddress",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
const val CRYPTO_CURRENCY_KEY = "currency"
const val TRANSACTION_ID_KEY = "transactionId"
const val AMOUNT_KEY = "amount"
const val TAG_KEY = "tag"
const val DESTINATION_ADDRESS_KEY = "destinationAddress"
}
}
)
@Serializable
data class Details(
@ -196,15 +183,7 @@ sealed class AppRoute(val path: String) : Route {
data class QrScanning(
val source: SourceType,
val networkName: String? = null,
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val SOURCE_KEY = "source"
const val NETWORK_KEY = "networkName"
}
}
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}")
@Serializable
data class ReferralProgram(
@ -230,18 +209,7 @@ sealed class AppRoute(val path: String) : Route {
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
}
)
@Serializable
data object TesterMenu : AppRoute(path = "/tester_menu")

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.annotation.OverridingMethodsMustInvokeSuper
/**
* Abstract class for a component's model.
@ -29,6 +30,7 @@ abstract class Model : InstanceKeeper.Instance {
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
}
@OverridingMethodsMustInvokeSuper
override fun onDestroy() {
runCatching { modelScope.cancel() }
}

View file

@ -9,6 +9,12 @@ android {
}
dependencies {
/** Domain models */
implementation(projects.domain.qrScanning.models)
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** AndroidX */
implementation(deps.androidx.fragment.ktx)

View file

@ -0,0 +1,15 @@
package com.tangem.feature.qrscanning
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.qrscanning.models.SourceType
interface QrScanningComponent : ComposableContentComponent {
data class Params(
val source: SourceType,
val networkName: String? = null,
)
interface Factory : ComponentFactory<Params, QrScanningComponent>
}

View file

@ -1,8 +0,0 @@
package com.tangem.feature.qrscanning
import androidx.fragment.app.Fragment
interface QrScanningRouter {
fun getEntryFragment(): Fragment
}

View file

@ -14,6 +14,7 @@ dependencies {
/** Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.common.routing)

View file

@ -0,0 +1,142 @@
package com.tangem.feature.qrscanning
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.model.QrScanningModel
import com.tangem.feature.qrscanning.presentation.QrScanningContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class DefaultQrScanningComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: QrScanningComponent.Params,
) : QrScanningComponent, AppComponentContext by appComponentContext {
private val model: QrScanningModel = getOrCreateModel(params)
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(model::onQrScanned)
}
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(model::onQrScanned)
}
init {
lifecycle.doOnDestroy { cameraExecutor.shutdown() }
}
@Composable
override fun Content(modifier: Modifier) {
val context = LocalContext.current
val cameraPermissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted.not()) {
model.onCameraDeniedState()
}
}
val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
val selectedImage = it ?: Uri.EMPTY
if (selectedImage != Uri.EMPTY) {
val mimeType = context.contentResolver.getType(selectedImage)
if (mimeType.isImageMimeType()) {
try {
val image = InputImage.fromFilePath(context, selectedImage)
analyzer.analyze(image)
} catch (e: IOException) {
Timber.e(e, "Unable to get image $selectedImage from gallery")
}
}
}
}
LaunchedEffect(Unit) {
model.launchGallery.collect {
galleryLauncher.launch(GALLERY_IMAGE_FILTER)
delay(timeMillis = 2000)
}
}
LifecycleEventEffect(
event = Lifecycle.Event.ON_CREATE,
) {
if (
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA,
) == PackageManager.PERMISSION_DENIED
) {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
LifecycleEventEffect(
event = Lifecycle.Event.ON_RESUME,
) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA,
) == PackageManager.PERMISSION_GRANTED
) {
model.onDismissBottomSheetState()
}
}
ScreenContent(modifier)
}
@Suppress("UnusedPrivateMember")
@Composable
private fun ScreenContent(modifier: Modifier = Modifier) {
SystemBarsIconsDisposable(darkIcons = false)
QrScanningContent(
executor = { cameraExecutor },
analyzer = { cameraAnalyzer },
uiState = model.uiState.collectAsStateWithLifecycle().value,
)
}
private fun String?.isImageMimeType() = this?.startsWith(prefix = "$IMAGE_MIME_TYPE/") == true
@AssistedFactory
interface Factory : QrScanningComponent.Factory {
override fun create(
context: AppComponentContext,
params: QrScanningComponent.Params,
): DefaultQrScanningComponent
}
companion object {
private const val IMAGE_MIME_TYPE = "image"
private const val GALLERY_IMAGE_FILTER = "$IMAGE_MIME_TYPE/*"
}
}

View file

@ -1,158 +0,0 @@
package com.tangem.feature.qrscanning
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningContent
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.properties.Delegates
@AndroidEntryPoint
internal class QrScanningFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var router: QrScanningRouter
private val innerRouter: QrScanningInnerRouter
get() = requireNotNull(router as? QrScanningInnerRouter) {
"innerRouter should be instance of QrScanningInnerRouter"
}
private val viewModel by viewModels<QrScanningViewModel>()
private var cameraExecutor: ExecutorService by Delegates.notNull()
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(viewModel::onQrScanned)
}
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
MLKitBarcodeAnalyzer(viewModel::onQrScanned)
}
private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (!it) viewModel.onCameraDeniedState()
}
private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
val selectedImage = it ?: Uri.EMPTY
if (selectedImage != Uri.EMPTY) {
val mimeType = requireContext().contentResolver.getType(selectedImage)
if (mimeType.isImageMimeType()) {
try {
val image = InputImage.fromFilePath(requireContext(), selectedImage)
analyzer.analyze(image)
} catch (e: IOException) {
Timber.e(e, "Unable to get image $selectedImage from gallery")
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.setRouter(innerRouter)
cameraExecutor = Executors.newSingleThreadExecutor()
requestCameraPermission()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.launchGalleryEvent
.collect {
galleryLauncher.launch(GALLERY_IMAGE_FILTER)
delay(timeMillis = 2000)
}
}
}
}
override fun onResume() {
super.onResume()
checkPermissionGranted()
}
override fun onDestroy() {
super.onDestroy()
cameraPermissionLauncher.unregister()
cameraExecutor.shutdown()
}
@Composable
override fun ScreenContent(modifier: Modifier) {
SystemBarsIconsDisposable(darkIcons = false)
QrScanningContent(
executor = { cameraExecutor },
analyzer = { cameraAnalyzer },
uiState = viewModel.uiState.collectAsStateWithLifecycle().value,
)
}
/**
* Method for requesting permission if there isn't one.
*/
private fun requestCameraPermission() {
if (
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA,
) == PackageManager.PERMISSION_DENIED
) {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
/**
* Method for checking if permission was granted after user opened Settings screen.
* If permission was granted dismiss bottom sheet.
*/
private fun checkPermissionGranted() {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA,
) == PackageManager.PERMISSION_GRANTED
) {
viewModel.onDismissBottomSheetState()
}
}
private fun String?.isImageMimeType() = this?.startsWith(prefix = "$IMAGE_MIME_TYPE/") == true
companion object {
private const val IMAGE_MIME_TYPE = "image"
private const val GALLERY_IMAGE_FILTER = "$IMAGE_MIME_TYPE/*"
fun create() = QrScanningFragment()
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.qrscanning.di
import com.tangem.core.decompose.model.Model
import com.tangem.feature.qrscanning.DefaultQrScanningComponent
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.qrscanning.model.QrScanningModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface QrScanningFeatureModule {
@Binds
fun bindComponentFactory(impl: DefaultQrScanningComponent.Factory): QrScanningComponent.Factory
@Binds
@IntoMap
@ClassKey(QrScanningModel::class)
fun bindModel(model: QrScanningModel): Model
}

View file

@ -1,21 +0,0 @@
package com.tangem.feature.qrscanning.di
import com.tangem.common.routing.AppRouter
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object QrScanningRouterModule {
@Provides
@ActivityScoped
fun provideQrScanRouter(appRouter: AppRouter): QrScanningRouter {
return DefaultQrScanningRouter(appRouter)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.feature.qrscanning.model
import kotlinx.coroutines.flow.SharedFlow
internal interface QrScanningClickIntents {
val launchGallery: SharedFlow<Unit>
fun onBackClick()
fun onQrScanned(qrCode: String)
fun onGalleryClicked()
fun onSettingsClick()
}

View file

@ -0,0 +1,91 @@
package com.tangem.feature.qrscanning.model
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ComponentScoped
internal class QrScanningModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val stateHolder: QrScanningStateController,
private val cardSdkProvider: CardSdkProvider,
private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase,
private val settingsManager: SettingsManager,
private val appRouter: AppRouter,
) : Model(), QrScanningClickIntents {
private val params = paramsContainer.require<QrScanningComponent.Params>()
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
private var isScanned = false
override val launchGallery = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST,
)
init {
// samsung for some reason disables reader mode, and then it works unstable
// to prevent this disable ir manually before scan QR
cardSdkProvider.sdk.forceDisableReaderMode()
stateHolder.update(InitializeQrScanningStateTransformer(this, params.source, params.networkName))
}
fun onCameraDeniedState() {
stateHolder.update(ShowCameraDeniedBottomSheetTransformer(this))
}
fun onDismissBottomSheetState() {
stateHolder.update(DismissBottomSheetTransformer())
}
override fun onBackClick() = appRouter.pop()
override fun onQrScanned(qrCode: String) {
if (qrCode.isNotBlank()) {
modelScope.launch(dispatchers.mainImmediate) {
emitQrScannedEventUseCase.invoke(params.source, qrCode)
}
if (!isScanned) {
appRouter.pop()
isScanned = true
}
}
}
override fun onGalleryClicked() {
launchGallery.tryEmit(Unit)
if (stateHolder.value.bottomSheetConfig != null) {
stateHolder.update(DismissBottomSheetTransformer())
}
}
override fun onSettingsClick() {
settingsManager.openAppSettings()
}
override fun onDestroy() {
super.onDestroy()
// don't forget enable reader mode after scan complete
cardSdkProvider.sdk.forceEnableReaderMode()
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.feature.qrscanning.navigation
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRouter
import com.tangem.feature.qrscanning.QrScanningFragment
class DefaultQrScanningRouter(
private val router: AppRouter,
) : QrScanningInnerRouter {
override fun getEntryFragment(): Fragment = QrScanningFragment.create()
override fun popBackStack() {
router.pop()
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.feature.qrscanning.navigation
import com.tangem.feature.qrscanning.QrScanningRouter
interface QrScanningInnerRouter : QrScanningRouter {
fun popBackStack()
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.impl.R
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents
import com.tangem.feature.qrscanning.model.QrScanningClickIntents
internal class InitializeQrScanningStateTransformer(
private val clickIntents: QrScanningClickIntents,

View file

@ -3,7 +3,7 @@ package com.tangem.feature.qrscanning.presentation.transformers
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.qrscanning.presentation.CameraDeniedBottomSheetConfig
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents
import com.tangem.feature.qrscanning.model.QrScanningClickIntents
internal class ShowCameraDeniedBottomSheetTransformer(
private val clickIntents: QrScanningClickIntents,

View file

@ -1,23 +0,0 @@
package com.tangem.feature.qrscanning.viewmodel
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import kotlinx.coroutines.CoroutineScope
import kotlin.properties.Delegates
internal open class BaseQrScanningClickIntents {
protected val router: QrScanningInnerRouter get() = _router
protected val viewModelScope: CoroutineScope get() = _viewModelScope
protected val source: SourceType get() = _source
private var _router: QrScanningInnerRouter by Delegates.notNull()
private var _viewModelScope: CoroutineScope by Delegates.notNull()
private var _source: SourceType by Delegates.notNull()
open fun initialize(router: QrScanningInnerRouter, source: SourceType, coroutineScope: CoroutineScope) {
_router = router
_viewModelScope = coroutineScope
_source = source
}
}

View file

@ -1,67 +0,0 @@
package com.tangem.feature.qrscanning.viewmodel
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
internal interface QrScanningClickIntents {
val launchGallery: SharedFlow<Unit>
fun onBackClick()
fun onQrScanned(qrCode: String)
fun onGalleryClicked()
fun onSettingsClick()
}
@ViewModelScoped
internal class QrScanningClickIntentsImplementor @Inject constructor(
private val stateHolder: QrScanningStateController,
private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase,
private val settingsManager: SettingsManager,
private val dispatcher: CoroutineDispatcherProvider,
) : BaseQrScanningClickIntents(), QrScanningClickIntents {
private var isScanned = false
override val launchGallery = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST,
)
override fun onBackClick() = router.popBackStack()
override fun onQrScanned(qrCode: String) {
if (qrCode.isNotBlank()) {
viewModelScope.launch(dispatcher.mainImmediate) {
emitQrScannedEventUseCase.invoke(source, qrCode)
}
if (!isScanned) {
router.popBackStack()
isScanned = true
}
}
}
override fun onGalleryClicked() {
launchGallery.tryEmit(Unit)
if (stateHolder.value.bottomSheetConfig != null) {
stateHolder.update(DismissBottomSheetTransformer())
}
}
override fun onSettingsClick() {
settingsManager.openAppSettings()
}
}

View file

@ -1,66 +0,0 @@
package com.tangem.feature.qrscanning.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@HiltViewModel
internal class QrScanningViewModel @Inject constructor(
private val stateHolder: QrScanningStateController,
private val clickIntents: QrScanningClickIntentsImplementor,
private val cardSdkProvider: CardSdkProvider,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val source: SourceType = savedStateHandle.get<Int>(AppRoute.QrScanning.SOURCE_KEY)
?.let { SourceType.entries[it] }
?: error("Source is mandatory")
private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY]
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
val launchGalleryEvent: SharedFlow<Unit> = clickIntents.launchGallery
init {
// samsung for some reason disables reader mode, and then it works unstable
// to prevent this disable ir manually before scan QR
cardSdkProvider.sdk.forceDisableReaderMode()
}
fun setRouter(router: QrScanningInnerRouter) {
clickIntents.initialize(
router = router,
source = source,
coroutineScope = viewModelScope,
)
stateHolder.update(InitializeQrScanningStateTransformer(clickIntents, source, network))
}
fun onQrScanned(qrCode: String) = clickIntents.onQrScanned(qrCode)
fun onCameraDeniedState() {
stateHolder.update(ShowCameraDeniedBottomSheetTransformer(clickIntents))
}
fun onDismissBottomSheetState() {
stateHolder.update(DismissBottomSheetTransformer())
}
override fun onCleared() {
super.onCleared()
// don't forget enable reader mode after scan complete
cardSdkProvider.sdk.forceEnableReaderMode()
}
}