Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-19 16:09:27 +00:00
commit 4b6c4381d4
9 changed files with 65 additions and 22 deletions

View file

@ -277,6 +277,7 @@ internal class BiometricUserWalletsListManager(
prevState.copy(
userWallets = newUserWallets,
encryptionKeys = encryptionKeys,
isLocked = newUserWallets.any(UserWallet::isLocked),
)
} else {

View file

@ -11,20 +11,20 @@ internal sealed interface SaveWalletAction : Action {
val backupCardsIds: Set<String>?,
) : SaveWalletAction
object Save : SaveWalletAction {
object Success : SaveWalletAction
data object Save : SaveWalletAction {
data object Success : SaveWalletAction
data class Error(val error: TangemError) : SaveWalletAction
}
object AllowToUseBiometrics : SaveWalletAction
data object AllowToUseBiometrics : SaveWalletAction
object Dismiss : SaveWalletAction
data object Dismiss : SaveWalletAction
object CloseError : SaveWalletAction
object EnrollBiometrics : SaveWalletAction {
object Enroll : SaveWalletAction
object Cancel : SaveWalletAction
data object CloseError : SaveWalletAction
data object EnrollBiometrics : SaveWalletAction {
data object Enroll : SaveWalletAction
data object Cancel : SaveWalletAction
}
object SaveWalletWasShown : SaveWalletAction
data object SaveWalletWasShown : SaveWalletAction
}

View file

@ -147,6 +147,11 @@ internal class SaveWalletMiddleware {
}
private fun allowToUseBiometrics(state: SaveWalletState) {
if (tangemSdkManager.needEnrollBiometrics) {
store.dispatchOnMain(SaveWalletAction.EnrollBiometrics)
return
}
val scanResponse = state.backupInfo?.scanResponse
?: store.state.globalState.scanResponse
?: return

View file

@ -20,10 +20,10 @@ import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.qrscanning.presentation.QrScanningContent
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
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 java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -47,6 +47,11 @@ internal class QrScanningFragment : ComposeFragment() {
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)
}
@ -69,6 +74,11 @@ internal class QrScanningFragment : ComposeFragment() {
requestCameraPermission()
}
override fun onResume() {
super.onResume()
checkPermissionGranted()
}
override fun onDestroy() {
super.onDestroy()
cameraPermissionLauncher.unregister()
@ -80,11 +90,14 @@ internal class QrScanningFragment : ComposeFragment() {
StatusBarTransparencyDisposable()
QrScanningContent(
executor = { cameraExecutor },
analyzer = { analyzer },
analyzer = { cameraAnalyzer },
uiState = viewModel.uiState.collectAsStateWithLifecycle().value,
)
}
/**
* Method for requesting permission if there isn't one.
*/
private fun requestCameraPermission() {
if (
ContextCompat.checkSelfPermission(
@ -96,6 +109,20 @@ internal class QrScanningFragment : ComposeFragment() {
}
}
/**
* 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()
}
}
companion object {
fun create() = QrScanningFragment()

View file

@ -36,7 +36,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) {
val intent: Intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null),
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
ContextCompat.startActivity(context, intent, null)
},
)

View file

@ -10,8 +10,9 @@ import com.tangem.feature.qrscanning.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.ShowCameraDeniedBottomSheetTransformer
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.StateFlow
import javax.inject.Inject
@ -43,4 +44,8 @@ internal class QrScanningViewModel @Inject constructor(
fun onCameraDeniedState() {
stateHolder.update(ShowCameraDeniedBottomSheetTransformer(clickIntents))
}
fun onDismissBottomSheetState() {
stateHolder.update(DismissBottomSheetTransformer())
}
}

View file

@ -703,6 +703,7 @@ internal class StateBuilder(
return uiState.copy(
sendCardData = uiState.sendCardData.copy(
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
token = cryptoCurrencyStatus,
),
)
}
@ -716,6 +717,7 @@ internal class StateBuilder(
return uiState.copy(
receiveCardData = uiState.receiveCardData.copy(
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
token = cryptoCurrencyStatus,
),
)
}

View file

@ -745,6 +745,8 @@ internal class SwapViewModel @Inject constructor(
dataState = dataState.copy(toCryptoCurrency = it)
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
}
startLoadingQuotesFromLastState(isSilent = true)
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)

View file

@ -177,14 +177,8 @@ internal class WalletViewModel @Inject constructor(
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name))
}
is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> {
stateHolder.clear()
router.popBackStack(screen = AppScreen.Welcome)
}
is WalletsUpdateActionResolver.Action.NoWallets -> {
stateHolder.clear()
router.popBackStack(screen = AppScreen.Home)
}
is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> closeScreen(screen = AppScreen.Welcome)
is WalletsUpdateActionResolver.Action.NoWallets -> closeScreen(screen = AppScreen.Home)
is WalletsUpdateActionResolver.Action.Unknown -> Unit
}
}
@ -295,6 +289,13 @@ internal class WalletViewModel @Inject constructor(
)
}
private fun closeScreen(screen: AppScreen) {
if (!screenLifecycleProvider.isBackground) {
stateHolder.clear()
router.popBackStack(screen = screen)
}
}
private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) {
stateHolder.update(
ScrollToWalletTransformer(