diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 72b9f67d82..9b8c16a128 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 970abaae15..2342a80e63 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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() }) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f2c6c70148..62df97b518 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -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") diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt index b1337c72f9..79ce712b57 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -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() } } diff --git a/features/qr-scanning/api/build.gradle.kts b/features/qr-scanning/api/build.gradle.kts index d88f0c6f03..5508ff8910 100644 --- a/features/qr-scanning/api/build.gradle.kts +++ b/features/qr-scanning/api/build.gradle.kts @@ -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) diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningComponent.kt b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningComponent.kt new file mode 100644 index 0000000000..4789912572 --- /dev/null +++ b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt deleted file mode 100644 index 049e039209..0000000000 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.feature.qrscanning - -import androidx.fragment.app.Fragment - -interface QrScanningRouter { - - fun getEntryFragment(): Fragment -} \ No newline at end of file diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index f8bb506a61..5e1a8a4c8f 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -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) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt new file mode 100644 index 0000000000..2e00a6910d --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt @@ -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/*" + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt deleted file mode 100644 index 787c7bdfe9..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ /dev/null @@ -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() - - 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() - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningFeatureModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningFeatureModule.kt new file mode 100644 index 0000000000..b623deb575 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningFeatureModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt deleted file mode 100644 index b22a6c713f..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt new file mode 100644 index 0000000000..b0702b0d62 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.qrscanning.model + +import kotlinx.coroutines.flow.SharedFlow + +internal interface QrScanningClickIntents { + + val launchGallery: SharedFlow + + fun onBackClick() + + fun onQrScanned(qrCode: String) + + fun onGalleryClicked() + + fun onSettingsClick() +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt new file mode 100644 index 0000000000..1ba92657ba --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt @@ -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() + val uiState: StateFlow = stateHolder.uiState + private var isScanned = false + + override val launchGallery = MutableSharedFlow( + 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() + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt deleted file mode 100644 index 3813274b68..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt deleted file mode 100644 index cdfb4cf247..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/QrScanningInnerRouter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.feature.qrscanning.navigation - -import com.tangem.feature.qrscanning.QrScanningRouter - -interface QrScanningInnerRouter : QrScanningRouter { - - fun popBackStack() -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index 2f38d3e545..7c6d08b333 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -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, diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt index 5dedeffebc..dfd650019f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt @@ -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, diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt deleted file mode 100644 index 2ff968422a..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt deleted file mode 100644 index adfaf8bcff..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ /dev/null @@ -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 - - 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( - 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() - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt deleted file mode 100644 index ceff5566d6..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ /dev/null @@ -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(AppRoute.QrScanning.SOURCE_KEY) - ?.let { SourceType.entries[it] } - ?: error("Source is mandatory") - private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY] - - val uiState: StateFlow = stateHolder.uiState - val launchGalleryEvent: SharedFlow = 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() - } -} \ No newline at end of file