diff --git a/app/src/main/java/com/vultisig/wallet/data/usecases/GetDirectionByQrCodeUseCase.kt b/app/src/main/java/com/vultisig/wallet/data/usecases/GetDirectionByQrCodeUseCase.kt new file mode 100644 index 000000000..6d4b15c17 --- /dev/null +++ b/app/src/main/java/com/vultisig/wallet/data/usecases/GetDirectionByQrCodeUseCase.kt @@ -0,0 +1,53 @@ +package com.vultisig.wallet.data.usecases + +import com.vultisig.wallet.data.common.JOIN_KEYGEN_FLOW +import com.vultisig.wallet.data.common.JOIN_KEYSIGN_FLOW +import com.vultisig.wallet.data.common.JOIN_SEND_ON_ADDRESS_FLOW +import com.vultisig.wallet.ui.navigation.Destination +import com.vultisig.wallet.ui.utils.getAddressFromQrCode +import com.vultisig.wallet.ui.utils.isReshare +import timber.log.Timber +import javax.inject.Inject +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +internal interface GetDirectionByQrCodeUseCase : suspend (String, String?) -> Destination + +internal class GetDirectionByQrCodeUseCaseImpl @Inject constructor( + private val getFlowType: GetFlowTypeUseCase, +) : GetDirectionByQrCodeUseCase { + @OptIn(ExperimentalEncodingApi::class) + override suspend fun invoke(qr: String, vaultId: String?): Destination { + Timber.d("joinOrSend(qr = $qr)") + val flowType = getFlowType(qr) + val qrBase64 = Base64.UrlSafe.encode(qr.toByteArray()) + return try { + when (flowType) { + JOIN_KEYSIGN_FLOW -> { + Destination.JoinKeysign( + vaultId = requireNotNull(vaultId), + qr = qrBase64, + ) + } + + JOIN_KEYGEN_FLOW -> { + Destination.JoinKeygen( + qr = qrBase64, + isReshare = qr.isReshare(), + ) + } + + JOIN_SEND_ON_ADDRESS_FLOW -> { + val address = qr.getAddressFromQrCode() + Destination.Send(vaultId = requireNotNull(vaultId), address = address) + } + + else -> Destination.ScanError + } + + } catch (e: Exception) { + Timber.e(e, "Failed to navigate to destination") + Destination.ScanError + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vultisig/wallet/data/usecases/UseCasesModule.kt b/app/src/main/java/com/vultisig/wallet/data/usecases/UseCasesModule.kt index f03e37705..ef06f25b8 100644 --- a/app/src/main/java/com/vultisig/wallet/data/usecases/UseCasesModule.kt +++ b/app/src/main/java/com/vultisig/wallet/data/usecases/UseCasesModule.kt @@ -75,4 +75,10 @@ internal interface UseCasesModule { fun bindGetSendDstByKeysignInitType( impl: GetSendDstByKeysignInitTypeImpl ): GetSendDstByKeysignInitType + + @Binds + @Singleton + fun bindGetDirectionByQrCodeUseCase( + impl: GetDirectionByQrCodeUseCaseImpl + ): GetDirectionByQrCodeUseCase } \ No newline at end of file diff --git a/app/src/main/java/com/vultisig/wallet/ui/models/ScanQrViewModel.kt b/app/src/main/java/com/vultisig/wallet/ui/models/ScanQrViewModel.kt index db4aab965..c6611ceea 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/models/ScanQrViewModel.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/models/ScanQrViewModel.kt @@ -3,82 +3,29 @@ package com.vultisig.wallet.ui.models import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vultisig.wallet.data.common.DeepLinkHelper +import com.vultisig.wallet.data.usecases.GetDirectionByQrCodeUseCase +import com.vultisig.wallet.data.usecases.GetFlowTypeUseCase import com.vultisig.wallet.ui.navigation.Destination import com.vultisig.wallet.ui.navigation.Navigator -import com.vultisig.wallet.ui.utils.getAddressFromQrCode -import com.vultisig.wallet.ui.utils.isJson -import com.vultisig.wallet.ui.utils.isReshare import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi @HiltViewModel internal class ScanQrViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val navigator: Navigator, + private val getFlowTypeUseCase: GetFlowTypeUseCase, + private val getDirectionByQrCodeUseCase: GetDirectionByQrCodeUseCase, ) : ViewModel() { private val vaultId: String? = savedStateHandle[Destination.ARG_VAULT_ID] - @OptIn(ExperimentalEncodingApi::class) - fun joinOrSend(qr: String) { - Timber.d("joinOrSend(qr = $qr)") - viewModelScope.launch { - val flowType = getFlowType(qr) - val qrBase64 = Base64.UrlSafe.encode(qr.toByteArray()) - try { - navigator.navigate( - when (flowType) { - JOIN_KEYSIGN_FLOW -> { - Destination.JoinKeysign( - vaultId = requireNotNull(vaultId), - qr = qrBase64, - ) - } - - JOIN_KEYGEN_FLOW -> { - Destination.JoinKeygen( - qr = qrBase64, - isReshare = qr.isReshare(), - ) - } - - JOIN_SEND_ON_ADDRESS_FLOW -> { - val address = qr.getAddressFromQrCode() - Destination.Send(vaultId = requireNotNull(vaultId), address = address) - } - - else -> Destination.ScanError - } - ) - } catch (e: Exception) { - Timber.e(e, "Failed to navigate to destination") - } - } + fun joinOrSend(qr: String) = viewModelScope.launch { + navigator.navigate(getDirectionByQrCodeUseCase(qr, vaultId)) } fun getFlowType(qr: String): String { - return try { - DeepLinkHelper(qr).getFlowType()?: throw IllegalArgumentException("No flowType found") - } catch (e: Exception) { - Timber.e(e, "Failed to parse QR-code via DeepLinkHelper") - if (qr.isJson()) { - UNKNOWN_FLOW - } else { - JOIN_SEND_ON_ADDRESS_FLOW - } - } + return getFlowTypeUseCase(qr) } - - companion object { - const val JOIN_KEYSIGN_FLOW = "SignTransaction" - const val JOIN_KEYGEN_FLOW = "NewVault" - const val JOIN_SEND_ON_ADDRESS_FLOW = "SendOnAddress" - const val UNKNOWN_FLOW = "Unknown" - } - } \ No newline at end of file diff --git a/app/src/main/java/com/vultisig/wallet/ui/models/VaultAccountsViewModel.kt b/app/src/main/java/com/vultisig/wallet/ui/models/VaultAccountsViewModel.kt index bd4396b0d..a118ff020 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/models/VaultAccountsViewModel.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/models/VaultAccountsViewModel.kt @@ -14,6 +14,7 @@ import com.vultisig.wallet.data.repositories.BalanceVisibilityRepository import com.vultisig.wallet.data.repositories.VaultDataStoreRepository import com.vultisig.wallet.data.repositories.VaultRepository import com.vultisig.wallet.data.repositories.vault.VaultMetadataRepo +import com.vultisig.wallet.data.usecases.GetDirectionByQrCodeUseCase import com.vultisig.wallet.data.usecases.IsGlobalBackupReminderRequiredUseCase import com.vultisig.wallet.data.usecases.NeverShowGlobalBackupReminderUseCase import com.vultisig.wallet.ui.models.mappers.AddressToUiModelMapper @@ -40,6 +41,7 @@ internal data class VaultAccountsUiModel( val isRefreshing: Boolean = false, val totalFiatValue: String? = null, val isBalanceValueVisible: Boolean = true, + val showCameraBottomSheet: Boolean = false, val accounts: List = emptyList(), ) { val isSwapEnabled = accounts.any { it.model.chain.IsSwapSupported } @@ -69,6 +71,7 @@ internal class VaultAccountsViewModel @Inject constructor( private val vaultMetadataRepo: VaultMetadataRepo, private val isGlobalBackupReminderRequired: IsGlobalBackupReminderRequiredUseCase, private val setNeverShowGlobalBackupReminder: NeverShowGlobalBackupReminderUseCase, + private val getDirectionByQrCodeUseCase: GetDirectionByQrCodeUseCase, ) : ViewModel() { private var vaultId: String? = null @@ -124,11 +127,8 @@ internal class VaultAccountsViewModel @Inject constructor( } } - fun joinKeysign() { - val vaultId = vaultId ?: return - viewModelScope.launch { - navigator.navigate(Destination.JoinThroughQr(vaultId = vaultId)) - } + fun openCamera() { + uiState.update { it.copy(showCameraBottomSheet = true) } } fun openAccount(account: AccountUiModel) { @@ -229,6 +229,15 @@ internal class VaultAccountsViewModel @Inject constructor( uiState.update { it.copy(showMonthlyBackupReminder = false) } } + fun dismissCameraBottomSheet() { + uiState.update { it.copy(showCameraBottomSheet = false) } + } + + fun onScanSuccess(qr: String) = viewModelScope.launch { + navigator.navigate(getDirectionByQrCodeUseCase(qr, vaultId)) + uiState.update { it.copy(showCameraBottomSheet = false) } + } + fun doNotRemindBackup() = viewModelScope.launch { setNeverShowGlobalBackupReminder() dismissBackupReminder() diff --git a/app/src/main/java/com/vultisig/wallet/ui/screens/home/VaultAccountsScreen.kt b/app/src/main/java/com/vultisig/wallet/ui/screens/home/VaultAccountsScreen.kt index a204ee555..1dd59c841 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/screens/home/VaultAccountsScreen.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/screens/home/VaultAccountsScreen.kt @@ -49,6 +49,7 @@ import com.vultisig.wallet.ui.models.AccountUiModel import com.vultisig.wallet.ui.models.VaultAccountsUiModel import com.vultisig.wallet.ui.models.VaultAccountsViewModel import com.vultisig.wallet.ui.navigation.Screen +import com.vultisig.wallet.ui.screens.scan.ScanQrBottomSheet import com.vultisig.wallet.ui.theme.Theme import kotlinx.coroutines.launch @@ -78,6 +79,12 @@ internal fun VaultAccountsScreen( onDoNotRemind = viewModel::doNotRemindBackup, ) } + if (state.showCameraBottomSheet) { + ScanQrBottomSheet ( + onDismiss = viewModel::dismissCameraBottomSheet, + onScanSuccess = viewModel::onScanSuccess, + ) + } VaultAccountsScreen( state = state, @@ -85,7 +92,7 @@ internal fun VaultAccountsScreen( onRefresh = viewModel::refreshData, onSend = viewModel::send, onSwap = viewModel::swap, - onJoinKeysign = viewModel::joinKeysign, + openCamera = viewModel::openCamera, onAccountClick = viewModel::openAccount, onChooseChains = { navHostController.navigate( @@ -106,7 +113,7 @@ private fun VaultAccountsScreen( onSend: () -> Unit = {}, onSwap: () -> Unit = {}, onRefresh: () -> Unit = {}, - onJoinKeysign: () -> Unit = {}, + openCamera: () -> Unit = {}, onAccountClick: (AccountUiModel) -> Unit = {}, onChooseChains: () -> Unit = {}, onToggleBalanceVisibility: () -> Unit = {}, @@ -238,7 +245,7 @@ private fun VaultAccountsScreen( size = 40.dp, contentDescription = "join keysign", tint = Theme.colors.oxfordBlue600Main, - onClick = onJoinKeysign, + onClick = openCamera, modifier = Modifier .background( color = Theme.colors.turquoise600Main, diff --git a/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrBottomSheet.kt b/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrBottomSheet.kt new file mode 100644 index 000000000..765df53b4 --- /dev/null +++ b/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrBottomSheet.kt @@ -0,0 +1,35 @@ +package com.vultisig.wallet.ui.screens.scan + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vultisig.wallet.ui.theme.Theme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScanQrBottomSheet ( + onDismiss: () -> Unit, + onScanSuccess: (qr: String) -> Unit, +) { + + ModalBottomSheet( + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ), + dragHandle = null, + containerColor = Theme.colors.transparent, + onDismissRequest = onDismiss, + ) { + Spacer(modifier = Modifier.height(100.dp)) + ScanQrScreen( + onScanSuccess = onScanSuccess, + roundedCorners = true, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrScreen.kt b/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrScreen.kt index 380935b66..c979c1438 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrScreen.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/screens/scan/ScanQrScreen.kt @@ -20,14 +20,17 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,6 +39,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource @@ -56,6 +60,7 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import com.vultisig.wallet.R +import com.vultisig.wallet.data.common.JOIN_SEND_ON_ADDRESS_FLOW import com.vultisig.wallet.ui.components.MultiColorButton import com.vultisig.wallet.ui.components.UiIcon import com.vultisig.wallet.ui.components.UiSpacer @@ -76,7 +81,7 @@ internal fun ScanQrAndJoin( viewModel: ScanQrViewModel = hiltViewModel(), ) { ScanQrScreen( - navController = navController, + onDismiss = { navController.popBackStack() }, onScanSuccess = viewModel::joinOrSend, ) } @@ -87,9 +92,9 @@ internal fun ScanQrScreen( viewModel: ScanQrViewModel = hiltViewModel(), ) { ScanQrScreen( - navController = navController, + onDismiss = { navController.popBackStack() }, onScanSuccess = { qr -> - if (viewModel.getFlowType(qr) == ScanQrViewModel.JOIN_SEND_ON_ADDRESS_FLOW) { + if (viewModel.getFlowType(qr) == JOIN_SEND_ON_ADDRESS_FLOW) { navController.previousBackStackEntry ?.savedStateHandle ?.set(ARG_QR_CODE, qr) @@ -102,7 +107,8 @@ internal fun ScanQrScreen( @OptIn(ExperimentalPermissionsApi::class) @Composable internal fun ScanQrScreen( - navController: NavController, + onDismiss: () -> Unit, + roundedCorners: Boolean = false, onScanSuccess: (qr: String) -> Unit, ) { val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) @@ -158,17 +164,18 @@ internal fun ScanQrScreen( bottom = 16.dp, ), text = stringResource(id = R.string.scan_qr_screen_return_vault), - onClick = { navController.popBackStack() }, + onClick = onDismiss, ) }, - ) { + ) { paddingValues -> Box( modifier = Modifier - .padding(it) + .padding(if (roundedCorners) PaddingValues(0.dp) else paddingValues) ) { if (cameraPermissionState.status.isGranted) { QrCameraScreen( + roundedCorners = roundedCorners, onSuccess = onSuccess, ) Image( @@ -256,6 +263,7 @@ internal fun ScanQrScreen( @Composable private fun QrCameraScreen( + roundedCorners: Boolean = false, onSuccess: (List) -> Unit, ) { @@ -265,8 +273,16 @@ private fun QrCameraScreen( ProcessCameraProvider.getInstance(localContext) } + DisposableEffect(Unit) { + onDispose { + unbindCameraListener(cameraProviderFuture, localContext) + } + } + AndroidView( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().let { + if (roundedCorners) it.clip(RoundedCornerShape(15.dp,15.dp, 0.dp, 0.dp)) else it + }, factory = { context -> val previewView = PreviewView(context) val resolutionStrategy = ResolutionStrategy( diff --git a/app/src/main/java/com/vultisig/wallet/ui/utils/StringX.kt b/app/src/main/java/com/vultisig/wallet/ui/utils/StringX.kt index 0e5056024..147ca74ea 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/utils/StringX.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/utils/StringX.kt @@ -18,10 +18,6 @@ internal fun String.isReshare(): Boolean { return contains("tssType=Reshare") } -internal fun String.isJson(): Boolean { - return startsWith("{") && endsWith("}") -} - internal fun List.groupByTwoButKeepFirstElement(): List { val listSize = this.size val originalList = this diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/common/QrFlowType.kt b/data/src/main/kotlin/com/vultisig/wallet/data/common/QrFlowType.kt new file mode 100644 index 000000000..25d4cd4e6 --- /dev/null +++ b/data/src/main/kotlin/com/vultisig/wallet/data/common/QrFlowType.kt @@ -0,0 +1,6 @@ +package com.vultisig.wallet.data.common + +const val JOIN_KEYSIGN_FLOW = "SignTransaction" +const val JOIN_KEYGEN_FLOW = "NewVault" +const val JOIN_SEND_ON_ADDRESS_FLOW = "SendOnAddress" +const val UNKNOWN_FLOW = "Unknown" \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/common/StringX.kt b/data/src/main/kotlin/com/vultisig/wallet/data/common/StringX.kt new file mode 100644 index 000000000..96750a607 --- /dev/null +++ b/data/src/main/kotlin/com/vultisig/wallet/data/common/StringX.kt @@ -0,0 +1,5 @@ +package com.vultisig.wallet.data.common + +fun String.isJson(): Boolean { + return startsWith("{") && endsWith("}") +} \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/usecases/DataUsecasesModule.kt b/data/src/main/kotlin/com/vultisig/wallet/data/usecases/DataUsecasesModule.kt index bfa197a48..74f784793 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/usecases/DataUsecasesModule.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/usecases/DataUsecasesModule.kt @@ -143,4 +143,10 @@ internal interface DataUsecasesModule { fun bindIsVaultHasFastSignUseCase( impl: IsVaultHasFastSignUseCaseImpl ): IsVaultHasFastSignUseCase + + @Binds + @Singleton + fun bindGetFlowTypeUseCase( + impl: GetFlowTypeUseCaseImpl + ): GetFlowTypeUseCase } \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/usecases/GetFlowTypeUseCase.kt b/data/src/main/kotlin/com/vultisig/wallet/data/usecases/GetFlowTypeUseCase.kt new file mode 100644 index 000000000..2a4d6cce1 --- /dev/null +++ b/data/src/main/kotlin/com/vultisig/wallet/data/usecases/GetFlowTypeUseCase.kt @@ -0,0 +1,25 @@ +package com.vultisig.wallet.data.usecases + +import com.vultisig.wallet.data.common.DeepLinkHelper +import com.vultisig.wallet.data.common.JOIN_SEND_ON_ADDRESS_FLOW +import com.vultisig.wallet.data.common.UNKNOWN_FLOW +import com.vultisig.wallet.data.common.isJson +import timber.log.Timber +import javax.inject.Inject + +interface GetFlowTypeUseCase : (String) -> String + +internal class GetFlowTypeUseCaseImpl @Inject constructor() : GetFlowTypeUseCase { + override fun invoke(qr: String): String { + return try { + DeepLinkHelper(qr).getFlowType()?: throw IllegalArgumentException("No flowType found") + } catch (e: Exception) { + Timber.e(e, "Failed to parse QR-code via DeepLinkHelper") + if (qr.isJson()) { + UNKNOWN_FLOW + } else { + JOIN_SEND_ON_ADDRESS_FLOW + } + } + } +} \ No newline at end of file