Updated on 2026-08-14
This commit is contained in:
parent
886dc6ee32
commit
4697ca6584
21 changed files with 323 additions and 412 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() })
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
interface QrScanningRouter {
|
||||
|
||||
fun getEntryFragment(): Fragment
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.navigation
|
||||
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
|
||||
interface QrScanningInnerRouter : QrScanningRouter {
|
||||
|
||||
fun popBackStack()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue