Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-03 02:55:23 +03:00
parent 44f6871822
commit 0802aa6b25
13 changed files with 271 additions and 33 deletions

View file

@ -108,6 +108,14 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
clipboard.setPrimaryClip(clip)
}
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
return clipData.getItemAt(0).text
}
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND

View file

@ -0,0 +1,65 @@
package com.tangem.tap.common.qrCodeScan
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.Result
import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE
import com.tangem.tap.features.send.redux.AddressPayIdActionUI
import com.tangem.tap.store
import me.dm7.barcodescanner.zxing.ZXingScannerView
/**
[REDACTED_AUTHOR]
*/
class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
private lateinit var mScannerView: ZXingScannerView
override fun onCreate(state: Bundle?) {
super.onCreate(state)
mScannerView = ZXingScannerView(this)
setContentView(mScannerView)
if (!permissionIsGranted()) requestPermission()
}
override fun onResume() {
super.onResume()
mScannerView.setResultHandler(this)
mScannerView.startCamera()
}
override fun onPause() {
super.onPause()
mScannerView.stopCamera()
}
override fun handleResult(result: Result) {
store.dispatch(AddressPayIdActionUI.SetAddressOrPayId(result.text))
finish()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != PERMISSION_REQUEST_CODE) return
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
finish()
}
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else true
}
private fun requestPermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.home.redux.homeMiddleware
import com.tangem.tap.features.send.redux.SendState
import com.tangem.tap.features.send.redux.logMiddleware
import com.tangem.tap.features.send.redux.sendMiddleware
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.walletMiddleware
import org.rekotlin.Middleware
@ -22,7 +22,7 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware,
homeMiddleware, walletMiddleware,
homeMiddleware, walletMiddleware, sendMiddleware
)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.send.redux
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
import timber.log.Timber

View file

@ -5,6 +5,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.send.redux.SendRelease
import com.tangem.tap.store
import kotlinx.android.synthetic.main.fragment_wallet.*
import org.rekotlin.StoreSubscriber
@ -42,4 +43,9 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
storeSubscribersList.forEach { store.unsubscribe(it) }
super.onStop()
}
override fun onDestroy() {
store.dispatch(SendRelease)
super.onDestroy()
}
}

View file

@ -1,14 +0,0 @@
package com.tangem.tap.features.send.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
interface SendAction : Action
sealed class FeeLayout : SendAction {
object ToggleFeeLayoutVisibility : FeeLayout()
data class ChangeSelectedFee(val id: Int) : FeeLayout()
class ChangeIncludeFee(val isChecked: Boolean) : FeeLayout()
}

View file

@ -0,0 +1,86 @@
package com.tangem.tap.features.send.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.AddressPayIdActionUI.SetAddressOrPayId
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
/**
[REDACTED_AUTHOR]
*/
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
handleSendAction(action)
nextDispatch(action)
}
}
}
private fun handleSendAction(action: Action) {
val sendAction = action as? SendScreenActionUI ?: return
when (sendAction) {
is AddressPayIdActionUI -> {
when (sendAction) {
is SetAddressOrPayId -> AddressPayIdHandler().handle(sendAction.data?.toString())
}
}
}
}
internal class AddressPayIdHandler {
fun handle(data: String?) {
val walletManager = store.state.globalState.walletManager ?: return
val clipboardData = data ?: return
if (AddressPayIDState.isPayIDAddress(clipboardData)) {
if (walletManager.wallet.blockchain.isPayIdSupported()) {
store.dispatch(AddressPayIdAction.Verification.PayIdNotSupportedByBlockchain)
} else {
scope.launch {
val response = verifyPayID(walletManager, clipboardData)
if (response == null) {
store.dispatch(AddressPayIdAction.Verification.Failed)
} else {
val address = "some address D:" // extract from response
store.dispatch(AddressPayIdAction.Verification.Success(address))
}
}
}
} else {
}
}
private suspend fun verifyPayID(walletManager: WalletManager, payID: String?): String? {
val cardId = walletManager.cardId
val publicKey = store.state.globalState.card?.cardPublicKey
// val result = PayIdManager().getPayId(cardId, publicKey.toHexString())
// withContext(Dispatchers.Main) {
// when (result) {
// is Result.Success -> {
// val payId = result.data
// if (payId == null) {
// store.dispatch(WalletAction.LoadPayId.NotCreated)
// } else {
// store.dispatch(WalletAction.LoadPayId.Success(payId))
// }
// }
// is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure)
// }
// }
return null
}
private suspend fun verifyWalletAddress(payID: String?): Boolean {
val isRealAddress = true
return isRealAddress
}
}

View file

@ -1,37 +1,59 @@
package com.tangem.tap.features.send.redux
import android.view.View
import com.tangem.tap.features.send.redux.AddressPayIdActionUI.SetAddressOrPayId
import com.tangem.tap.features.send.redux.FeeActionUI.*
import org.rekotlin.Action
import org.rekotlin.StateType
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SendReducer {
companion object {
fun reduce(action: Action, state: SendState): SendState {
val sendAction = action as? SendAction ?: return state
fun reduce(action: Action, sendState: SendState): SendState {
if (action is SendRelease) return SendState()
val sendAction = action as? SendScreenAction ?: return sendState
return when (sendAction) {
is FeeLayout -> handleFeeLayoutAction(sendAction, state, state.feeLayoutState)
else -> state
is AddressPayIdActionUI -> handleAddressPayIdAction(sendAction, sendState, sendState.addressPayIDState)
is FeeActionUI -> handleFeeLayoutAction(sendAction, sendState, sendState.feeLayoutState)
else -> sendState
}
}
private fun handleFeeLayoutAction(action: FeeLayout, sendState: SendState, state: FeeLayoutState): SendState {
private fun handleAddressPayIdAction(
action: AddressPayIdActionUI,
sendState: SendState,
state: AddressPayIDState
): SendState {
var state = state
when (action) {
is SetAddressOrPayId -> {
state = state.copy(value = action.data?.toString())
return updateLastState(sendState.copy(addressPayIDState = state), state)
}
}
return sendState
}
private fun handleFeeLayoutAction(action: FeeActionUI, sendState: SendState, state: FeeLayoutState): SendState {
return when (action) {
is FeeLayout.ToggleFeeLayoutVisibility -> {
is ToggleFeeLayoutVisibility -> {
val visibility = if (state.visibility == View.VISIBLE) View.GONE
else View.VISIBLE
val result = state.copy(visibility = visibility)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
is FeeLayout.ChangeSelectedFee -> {
is ChangeSelectedFee -> {
val result = state.copy(selectedFeeId = action.id)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
is FeeLayout.ChangeIncludeFee -> {
is ChangeIncludeFee -> {
val result = state.copy(includeFeeIsChecked = action.isChecked)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
@ -39,7 +61,9 @@ class SendReducer {
}
private fun updateLastState(sendState: SendState, state: StateType): SendState {
return sendState.copy(lastChangedStateType = state)
val sendState = sendState.copy(lastChangedStateType = state)
Timber.d("$sendState")
return sendState
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.send.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
interface SendScreenAction : Action
interface SendScreenActionUI : SendScreenAction
object SendRelease : Action
sealed class FeeActionUI : SendScreenActionUI {
object ToggleFeeLayoutVisibility : FeeActionUI()
data class ChangeSelectedFee(val id: Int) : FeeActionUI()
class ChangeIncludeFee(val isChecked: Boolean) : FeeActionUI()
}
sealed class AddressPayIdActionUI : SendScreenActionUI {
data class SetAddressOrPayId(val data: CharSequence?) : AddressPayIdActionUI()
}
sealed class AddressPayIdAction : SendScreenAction {
object Verification : AddressPayIdAction() {
object PayIdNotSupportedByBlockchain : AddressPayIdAction()
object Failed : AddressPayIdAction()
data class Success(val payIdWalletAddress: String) : AddressPayIdAction()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.send.redux
import android.view.View
import com.tangem.blockchain.common.WalletManager
import com.tangem.wallet.R
import org.rekotlin.StateType
@ -8,12 +9,26 @@ import org.rekotlin.StateType
[REDACTED_AUTHOR]
*/
data class SendState(
val walletManager: WalletManager? = null,
val lastChangedStateType: StateType = NoneState(),
val addressPayIDState: AddressPayIDState = AddressPayIDState(),
val feeLayoutState: FeeLayoutState = FeeLayoutState()
) : StateType
class NoneState : StateType
data class AddressPayIDState(
val value: String? = null,
val payIDWalletAddress: String? = null,
val error: String? = null,
) : StateType {
fun isPayIDAddress(): Boolean = isPayIDAddress(value)
companion object {
fun isPayIDAddress(value: String?): Boolean = value?.contains("\$payid.tangem.com") ?: false
}
}
data class FeeLayoutState(
val visibility: Int = View.GONE,
val selectedFeeId: Int = R.id.chipNormal,

View file

@ -1,13 +1,19 @@
package com.tangem.tap.features.send.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.send.redux.FeeLayout
import com.tangem.tap.features.send.redux.AddressPayIdActionUI.SetAddressOrPayId
import com.tangem.tap.features.send.redux.FeeActionUI.*
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.send.ui.stateSubscribers.WalletStateSubscriber
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.btn_paste.*
import kotlinx.android.synthetic.main.btn_qr_code.*
import kotlinx.android.synthetic.main.layout_send_network_fee.*
/**
@ -22,13 +28,20 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
super.onViewCreated(view, savedInstanceState)
flExpandCollapse.setOnClickListener {
store.dispatch(FeeLayout.ToggleFeeLayoutVisibility)
store.dispatch(ToggleFeeLayoutVisibility)
}
chipGroup.setOnCheckedChangeListener { group, checkedId ->
store.dispatch(FeeLayout.ChangeSelectedFee(checkedId))
store.dispatch(ChangeSelectedFee(checkedId))
}
swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
store.dispatch(FeeLayout.ChangeIncludeFee(isChecked))
store.dispatch(ChangeIncludeFee(isChecked))
}
imvPaste.setOnClickListener {
store.dispatch(SetAddressOrPayId(requireContext().getFromClipboard()))
}
imvQrCode.setOnClickListener {
requireActivity().startActivity(Intent(requireContext(), ScanQrCodeActivity::class.java))
}
}

View file

@ -3,11 +3,12 @@ package com.tangem.tap.features.send.ui.stateSubscribers
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.transition.TransitionManager
import com.tangem.tap.features.send.redux.AddressPayIDState
import com.tangem.tap.features.send.redux.FeeLayoutState
import com.tangem.tap.features.send.redux.SendState
import kotlinx.android.synthetic.main.btn_expand_collapse.*
import kotlinx.android.synthetic.main.layout_send_address_payid.*
import kotlinx.android.synthetic.main.layout_send_network_fee.*
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -17,11 +18,15 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
override fun updateWithNewState(fg: Fragment, state: SendState) {
when (state.lastChangedStateType) {
is FeeLayoutState -> handleFeeLayoutState(fg, state.feeLayoutState)
is AddressPayIDState -> handleAddressPayIdState(fg, state.addressPayIDState)
}
}
private fun handleAddressPayIdState(fg: Fragment, state: AddressPayIDState) {
fg.etAddressOrPayId.setText(state.value)
}
private fun handleFeeLayoutState(fg: Fragment, layoutState: FeeLayoutState) {
Timber.d("handleFeeLayoutState")
if (fg.llFeeContainer.visibility != layoutState.visibility) {
val rotationAngle = if (fg.imvExpandCollapse.rotation == 0f) 180f else 0f
fg.imvExpandCollapse.rotation = rotationAngle