diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b2ede6c22..99243416f0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,8 @@ android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" /> + + \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 06bd7b850e..e08ffeb499 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt new file mode 100644 index 0000000000..b350c9ebe4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -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, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 2a03f03e25..045c802a04 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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> { return listOf( logMiddleware, navigationMiddleware, notificationsMiddleware, - homeMiddleware, walletMiddleware, + homeMiddleware, walletMiddleware, sendMiddleware ) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt similarity index 75% rename from app/src/main/java/com/tangem/tap/features/send/redux/LogMiddleware.kt rename to app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 3d6f5b59d6..9e7276f9bd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt index 20669d7f3b..c3c591a1c4 100644 --- a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendAction.kt deleted file mode 100644 index 1e28b71713..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendAction.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt new file mode 100644 index 0000000000..8b9800c188 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt @@ -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 = { 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt index 69c58dde89..2cf6e9fe78 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt @@ -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 } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt new file mode 100644 index 0000000000..3da6e6acce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt index e2e5c5aa38..d17bab98c9 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 40f2689a18..06929486da 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -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)) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 3f4ea153f4..d86fe32991 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -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 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