Updated on 2026-08-14
This commit is contained in:
parent
96aebcdba2
commit
477aa3c2c4
42 changed files with 447 additions and 166 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.details.redux.walletconnect
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
|
|
@ -16,6 +17,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.walletconnect.WalletConnectActions
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -98,7 +101,14 @@ class WalletConnectMiddleware {
|
|||
if (uri != null && isWalletConnectUri(uri)) {
|
||||
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
|
||||
store.dispatchOnMain(
|
||||
NavigationAction.NavigateTo(
|
||||
screen = AppScreen.QrScanning,
|
||||
bundle = bundleOf(
|
||||
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
is WalletConnectAction.SelectNetwork -> {
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.otaliastudios.cameraview.CameraView
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.LayoutQrScanningBinding
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal class QrScanFragment : Fragment(R.layout.layout_qr_scanning) {
|
||||
|
||||
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
|
||||
|
||||
private val binder = PreviewBinder()
|
||||
|
||||
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
||||
private var cameraExecutor: ExecutorService? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setFitSystemWindows(fit = true)
|
||||
activity?.onBackPressedDispatcher?.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (!permissionIsGranted()) requestPermission()
|
||||
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
cameraProviderFuture?.addListener(
|
||||
{
|
||||
val cameraProvider = cameraProviderFuture?.get()
|
||||
binder.bindPreview(
|
||||
context = requireContext(),
|
||||
binding = binding,
|
||||
lifecycleOwner = this,
|
||||
cameraProvider = requireNotNull(cameraProvider),
|
||||
cameraExecutor = requireNotNull(cameraExecutor),
|
||||
onScanned = { result ->
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
setFitSystemWindows(fit = false)
|
||||
if (result.isNotBlank()) {
|
||||
store.dispatch(WalletConnectAction.OpenSession(result))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
ContextCompat.getMainExecutor(requireContext()),
|
||||
)
|
||||
|
||||
binding.overlay.post {
|
||||
binding.overlay.setViewFinder()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
setFitSystemWindows(fit = false)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return
|
||||
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
store.dispatch(WalletConnectAction.NotifyCameraPermissionIsRequired)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
}
|
||||
|
||||
private fun permissionIsGranted(): Boolean {
|
||||
val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
|
||||
return cameraPermission == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
private fun setFitSystemWindows(fit: Boolean) {
|
||||
activity?.window?.let {
|
||||
WindowCompat.setDecorFitsSystemWindows(it, fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
@ -23,22 +24,25 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<Wallet
|
|||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel = WalletConnectViewModel(store)
|
||||
private var screenState: MutableState<WalletConnectScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
|
||||
private val viewModel: WalletConnectViewModel by viewModels()
|
||||
|
||||
private var screenState: MutableState<WalletConnectScreenState>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(WalletConnect.ScreenOpened())
|
||||
lifecycle.addObserver(viewModel)
|
||||
screenState = mutableStateOf(viewModel.updateState(store.state.walletConnectState))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state = screenState?.value ?: return
|
||||
WalletConnectScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
state = state,
|
||||
onBackClick = {
|
||||
if (screenState.value.isLoading) {
|
||||
if (state.isLoading) {
|
||||
store.dispatch(
|
||||
WalletConnectAction.FailureEstablishingSession(
|
||||
store.state.walletConnectState.newSessionData?.session?.session,
|
||||
|
|
@ -66,6 +70,6 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<Wallet
|
|||
|
||||
override fun newState(state: WalletConnectState) {
|
||||
if (activity == null || view == null) return
|
||||
screenState.value = viewModel.updateState(state)
|
||||
screenState?.value = viewModel.updateState(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,34 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.Store
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class WalletConnectViewModel @Inject constructor(
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
viewModelScope.launch {
|
||||
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
|
||||
.getOrElse { emptyFlow() }
|
||||
.flowWithLifecycle(owner.lifecycle, minActiveState = Lifecycle.State.CREATED)
|
||||
.collect { store.dispatch(WalletConnectAction.OpenSession(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
internal class WalletConnectViewModel(private val store: Store<AppState>) {
|
||||
fun updateState(state: WalletConnectState): WalletConnectScreenState {
|
||||
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
|
||||
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs
|
|||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -19,7 +22,14 @@ object ClipboardOrScanQrDialog {
|
|||
store.dispatch(WalletConnectAction.OpenSession(wcUri))
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan))
|
||||
store.dispatch(
|
||||
NavigationAction.NavigateTo(
|
||||
screen = AppScreen.QrScanning,
|
||||
bundle = bundleOf(
|
||||
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Size
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.Surface
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.tangem.tap.common.qrCodeScan.MLKitBarcodeAnalyzer
|
||||
import com.tangem.wallet.databinding.LayoutQrScanningBinding
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
internal class PreviewBinder {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun bindPreview(
|
||||
context: Context,
|
||||
binding: LayoutQrScanningBinding,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
cameraProvider: ProcessCameraProvider,
|
||||
cameraExecutor: ExecutorService,
|
||||
onScanned: (String) -> Unit,
|
||||
) {
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
val preview: Preview = Preview.Builder()
|
||||
.build()
|
||||
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val orientationEventListener = object : OrientationEventListener(context) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
val rotation: Int = when (orientation) {
|
||||
in 45..134 -> Surface.ROTATION_270
|
||||
in 135..224 -> Surface.ROTATION_180
|
||||
in 225..314 -> Surface.ROTATION_90
|
||||
else -> Surface.ROTATION_0
|
||||
}
|
||||
|
||||
imageAnalysis.targetRotation = rotation
|
||||
}
|
||||
}
|
||||
orientationEventListener.enable()
|
||||
|
||||
val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer {
|
||||
imageAnalysis.clearAnalyzer()
|
||||
onScanned.invoke(it)
|
||||
}
|
||||
|
||||
cameraExecutor.let {
|
||||
imageAnalysis.setAnalyzer(it, analyzer)
|
||||
}
|
||||
|
||||
preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
|
||||
|
||||
val cameraSelector: CameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,30 +3,38 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import arrow.core.getOrElse
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.tangem.Message
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
|
||||
import com.tangem.sdk.extensions.hideSoftKeyboard
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.tap.common.extensions.setOnImeActionListener
|
||||
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
|
||||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
|
||||
import com.tangem.tap.common.text.truncateMiddleWith
|
||||
|
|
@ -49,8 +57,11 @@ import com.tangem.wallet.databinding.FragmentSendBinding
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormatSymbols
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
||||
|
||||
|
|
@ -73,11 +84,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
|
||||
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
|
||||
|
||||
@Inject
|
||||
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
sendSubscriber.initViewModel(viewModel)
|
||||
Analytics.send(Token.Send.ScreenOpened())
|
||||
listenToQrCode()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
@ -145,13 +160,34 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
}
|
||||
imvQrCode.setOnClickListener {
|
||||
Analytics.send(Token.Send.ButtonQRCode())
|
||||
startActivityForResult(
|
||||
Intent(requireContext(), ScanQrCodeActivity::class.java),
|
||||
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE,
|
||||
|
||||
store.dispatchOnMain(
|
||||
NavigationAction.NavigateTo(
|
||||
screen = AppScreen.QrScanning,
|
||||
bundle = bundleOf(
|
||||
QrScanningRouter.SOURCE_KEY to SourceType.SEND,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenToQrCode() {
|
||||
lifecycleScope.launch {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
|
||||
.collect {
|
||||
delay(200)
|
||||
|
||||
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
|
||||
// If do not use the delay, then etAmount error field is not displayed when
|
||||
// inserting an incorrect amount by shareUri
|
||||
onCodeScanned(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
|
||||
// TODO: [REDACTED_TASK_KEY]
|
||||
etXlmMemo.inputtedTextAsFlow()
|
||||
|
|
@ -200,27 +236,16 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return
|
||||
|
||||
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
|
||||
private fun onCodeScanned(scannedCode: String) {
|
||||
if (scannedCode.isEmpty()) return
|
||||
|
||||
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
|
||||
// If do not use the delay, then etAmount error field is not displayed when
|
||||
// inserting an incorrect amount by shareUri
|
||||
binding.lSendAddress.imvQrCode.postDelayed(
|
||||
{
|
||||
store.dispatch(
|
||||
PasteAddress(
|
||||
data = scannedCode,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
|
||||
},
|
||||
200,
|
||||
store.dispatch(
|
||||
PasteAddress(
|
||||
data = scannedCode,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
|
||||
}
|
||||
|
||||
private fun setupAmountLayout() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue