Updated on 2026-08-14

This commit is contained in:
Tangem 2021-07-26 06:35:48 +00:00
commit 025ffe01f1
46 changed files with 747 additions and 206 deletions

View file

@ -81,9 +81,33 @@
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wc"/>
<data android:scheme="wc" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="success.tangem.com"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="sell-request.tangem.com"
android:scheme="tangem" />
</intent-filter>
</activity>

View file

@ -0,0 +1,21 @@
package com.tangem.tap.common
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.wallet.R
class CustomTabsManager {
fun openUrl(url: String, context: Context) {
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(context.getColorCompat(R.color.toolbarColor))
.build()
)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}

View file

@ -5,16 +5,20 @@ import android.nfc.NfcAdapter
import android.nfc.Tag
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import timber.log.Timber
class IntentHandler {
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
handleSellCurrencyCallback(intent)
}
private fun handleBackgroundScan(intent: Intent?) {
@ -38,4 +42,26 @@ class IntentHandler {
}
}
private fun handleSellCurrencyCallback(intent: Intent?) {
val transactionID =
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID
))
}
}

View file

@ -30,6 +30,11 @@ fun Fragment.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(requireContext(), colorRes)
}
@ColorInt
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -43,4 +44,8 @@ sealed class GlobalAction : Action {
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
object GetMoonPayUserStatus : GlobalAction() {
data class Success(val moonPayUserStatus: MoonPayUserStatus) : GlobalAction()
}
}

View file

@ -1,14 +1,19 @@
package com.tangem.tap.common.redux.global
import com.tangem.TangemSdkError
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.moonpay.MoonpayService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
@ -60,6 +65,19 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
scope.launch {
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
)
}
}
}
}
}
nextDispatch(action)
}

View file

@ -73,6 +73,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayUserStatus.Success -> {
globalState.copy(moonPayUserStatus = action.moonPayUserStatus)
}
else -> globalState
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.StateType
data class GlobalState(
@ -22,7 +23,8 @@ data class GlobalState(
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null
) : StateType
typealias CryptoCurrencyName = String

View file

@ -10,6 +10,8 @@ sealed class NavigationAction : Action {
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}

View file

@ -1,25 +1,30 @@
package com.tangem.tap.common.redux.navigation
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
val navState = store.state.navigationState
val navState = state()?.navigationState
when (action) {
is NavigationAction.NavigateTo -> {
navState.activity?.get()?.openFragment(action.screen, action.addToBackstack)
navState?.activity?.get()?.openFragment(action.screen, action.addToBackstack)
}
is NavigationAction.PopBackTo -> {
if (action.screen == AppScreen.Home) {
navState.activity?.get()?.popBackTo(null, true)
navState?.activity?.get()?.popBackTo(null, true)
} else {
navState.activity?.get()?.popBackTo(action.screen)
navState?.activity?.get()?.popBackTo(action.screen)
}
}
is NavigationAction.OpenUrl -> {
navState?.activity?.get()?.let {
CustomTabsManager().openUrl(action.url, it)
}
}
}

View file

@ -27,5 +27,6 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
}
is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity)
is NavigationAction.ActivityDestroyed -> navState.copy(activity = null)
else -> navState
}
}

View file

@ -174,7 +174,11 @@ class TapWalletManager {
}
}
store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id))
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate())
}
@ -245,7 +249,11 @@ class TapWalletManager {
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
}

View file

@ -20,7 +20,7 @@ object TapWorkarounds {
val cardData = this.cardData ?: return false
val productMask = cardData.productMask
val excludedBatch = excludedBatches.contains(cardData.batchId)
val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.toUpperCase(Locale.US))
val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.uppercase(Locale.ROOT))
val excludedProductMask = (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard))
return excludedBatch || excludedIssuerName || excludedProductMask

View file

@ -1,55 +0,0 @@
package com.tangem.tap.domain.topup
import android.net.Uri
import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TopUpHelper {
companion object {
// const val REDIRECT_URL = "https://success.tangem.com"
private const val BASE_URL = "https://buy.moonpay.io"
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
// private const val REDIRECT_URL_PATH = "&redirectUrl="
private const val SIGNATURE_PATH = "&signature="
fun getUrl(
blockchain: Blockchain?,
cryptoCurrencyName: CryptoCurrencyName,
walletAddress: String,
apiKey: String,
secretKey: String,
): String {
if (blockchain?.isTestnet() == true) {
return blockchain.getTestnetTopUpUrl() ?: ""
}
val originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode()
// REDIRECT_URL_PATH + REDIRECT_URL.urlEncode()
val signature = createSignature(originalQuery, secretKey)
return BASE_URL + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.domain.topup
import android.net.Uri
import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TradeCryptoHelper {
companion object {
const val TRANSACTION_ID_PARAM = "transactionId"
const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val REDIRECT_URL_BUY = "tangem://success.tangem.com"
private const val REDIRECT_URL_SELL_REQUEST = "tangem://sell-request.tangem.com"
private const val BASE_URL_BUY =
"https://buy.moonpay.io"
// "https://buy-staging.moonpay.io" // TESTNET
private const val BASE_URL_SELL =
"https://sell.moonpay.com"
// "https://sell-staging.moonpay.com" // TESTNET
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
private const val REFUND_WALLET_ADDRESS_PATH = "&refundWalletAddress="
private const val BASE_CURRENCY_PATH = "&baseCurrencyCode="
private const val REDIRECT_URL_PATH = "&redirectURL="
private const val SIGNATURE_PATH = "&signature="
private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId="
val AVAILABLE_TO_BUY: Set<String> = setOf(
"ZRX", "AAVE", "ALGO", "AXS", "BAT", "BNB", "BUSD", "BTC", "BCH", "BTT", "ADA", "CELO", "CUSD", "LINK", "CHZ", "COMP", "ATOM", "DAI", "DASH", "MANA", "DGB", "DOGE", "EGLD",
"ENJ", "EOS", "ETC", "ETH", "KETH", "RINKETH", "FIL", "HBAR", "MIOTA", "KAVA", "KLAY", "LBC", "LTC", "LUNA", "MKR", "OM", "MATIC", "NANO", "NEAR", "XEM", "NEO", "NIM", "OKB",
"OMG", "ONG", "ONT", "DOT", "QTUM", "RVN", "RFUEL", "KEY", "SRM", "SOL", "XLM", "STMX", "SNX", "KRT", "UST", "USDT", "XTZ", "RUNE", "SAND", "TOMO", "AVA", "TRX", "TUSD", "UNI",
"USDC", "UTK", "VET", "WAXP", "WBTC", "XRP", "ZEC", "ZIL"
)
val AVAILABLE_TO_SELL: Set<String> = setOf("BTC", "ETH", "BCH")
fun getUrl(
action: Action,
blockchain: Blockchain?,
cryptoCurrencyName: CryptoCurrencyName,
walletAddress: String,
apiKey: String,
secretKey: String,
): String {
val originalQuery: String
val baseUrl: String
when (action) {
Action.Buy -> {
if (blockchain?.isTestnet() == true) {
return blockchain.getTestnetTopUpUrl() ?: ""
}
baseUrl = BASE_URL_BUY
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode()
}
Action.Sell -> {
baseUrl = BASE_URL_SELL
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
BASE_CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
REFUND_WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_SELL_REQUEST.urlEncode()
}
}
val signature = createSignature(originalQuery, secretKey)
return baseUrl + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
fun getSellCryptoReceiptUrl(transactionId: String): String {
return BASE_URL_SELL + TRANSACTION_RECEIPT_PATH + transactionId
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
enum class Action { Buy, Sell }
}

View file

@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeDialog
@ -66,6 +67,8 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
btn_yes?.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_shop?.setOnClickListener { store.dispatch(HomeAction.GoToShop(requireContext())) }
store.dispatch(GlobalAction.GetMoonPayUserStatus)
}
override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
@ -28,7 +29,14 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
)
}
}
})
val inflater = TransitionInflater.from(requireContext())
@ -43,7 +51,7 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
}
override fun onStart() {

View file

@ -132,6 +132,7 @@ sealed class ReceiptAction : SendScreenAction {
sealed class SendActionUi : SendScreenActionUi {
data class SendAmountToRecipient(val messageForSigner: Message) : SendScreenActionUi
object CheckIfTransactionDataWasProvided : SendScreenActionUi
}
sealed class SendAction : SendScreenAction {
@ -158,4 +159,11 @@ sealed class SendAction : SendScreenAction {
object Update : SendAction()
data class Set(val warningList: List<WarningMessage>) : SendAction()
}
data class SendSpecificTransaction(
val sendAmount: String,
val destinationAddress: String,
val transactionId: String
) : SendAction()
}

View file

@ -9,6 +9,7 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,6 +21,8 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
@ -49,6 +52,18 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
transactionData.destinationAddress, false
))
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
store.dispatchOnMain(AmountAction.SetAmount(transactionData.amount.toBigDecimal(),
false))
}
}
}
nextDispatch(action)
}
@ -76,8 +91,11 @@ private fun verifyAndSendTransaction(
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend)
}, sendAllCallback = {
sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, dispatch)
sendTransaction(
action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, sendState.externalTransactionData,
dispatch
)
}, reduceAmount))
}
transactionErrors.isNotEmpty() -> {
@ -85,7 +103,7 @@ private fun verifyAndSendTransaction(
}
else -> {
sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, dispatch)
sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch)
}
}
}
@ -98,6 +116,7 @@ private fun sendTransaction(
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
externalTransactionData: ExternalTransactionData?,
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
@ -134,7 +153,12 @@ private fun sendTransaction(
blockchain = walletManager.wallet.blockchain
)
dispatch(SendAction.SendSuccess)
dispatch(NavigationAction.PopBackTo())
if (externalTransactionData != null) {
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain))

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.IdStateHolder
import com.tangem.tap.features.send.redux.states.SendState
import org.rekotlin.Action
@ -45,11 +46,31 @@ private class SendReducer : SendInternalReducer {
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
is SendAction.SendSpecificTransaction ->
handleSendSpecificTransactionAction(action, sendState)
else -> return sendState
}
return updateLastState(result, result)
}
private fun handleSendSpecificTransactionAction(
action: SendAction.SendSpecificTransaction, state: SendState
): SendState {
return state.copy(
externalTransactionData =
ExternalTransactionData(action.sendAmount, action.destinationAddress, action.transactionId),
feeState = state.feeState.copy(
includeFeeSwitcherIsEnabled = false
),
amountState = state.amountState.copy(
inputIsEnabled = false
),
addressPayIdState = state.addressPayIdState.copy(
inputIsEnabled = false
),
)
}
}
private class EmptyReducer : SendInternalReducer {

View file

@ -12,7 +12,8 @@ data class AddressPayIdState(
val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true
) : SendScreenState {
override val stateId: StateId = StateId.ADDRESS_PAY_ID

View file

@ -40,7 +40,8 @@ data class SendState(
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
val dialog: StateDialog? = null
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN
@ -131,7 +132,8 @@ data class AmountState(
val cursorAtTheSamePosition: Boolean = true,
val maxLengthOfAmount: Int = 2,
val decimalSeparator: String = ".",
val error: TapError? = null
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
@ -166,4 +168,12 @@ data class MainCurrency(
val type: MainCurrencyType,
val currencySymbol: String,
val isEnabled: Boolean = true
)
data class ExternalTransactionData(
val amount: String,
val destinationAddress: String,
val transactionId: String,
val canAmountBeModified: Boolean = false,
val canDestinationAddressBeModified: Boolean = false
)

View file

@ -71,6 +71,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
setupAmountLayout()
setupFeeLayout()
setupWarningMessages()
store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided)
}
private fun initSendButtonStates() {
@ -250,7 +251,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
val mainCurrency = sp.getString("mainCurrency", TapCurrency.DEFAULT_FIAT_CURRENCY)
val foundType = MainCurrencyType.values()
.firstOrNull { it.name.toLowerCase() == mainCurrency!!.toLowerCase() }
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
?: MainCurrencyType.CRYPTO
return foundType
}

View file

@ -156,6 +156,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val til = fg.tilAddressOrPayId
val parsedError = parseError(til.context, state.error)
til.isEnabled = state.inputIsEnabled
fg.imvPaste.show(state.inputIsEnabled)
fg.imvQrCode.show(state.inputIsEnabled)
fg.flPaste.show(state.inputIsEnabled)
fg.flQrCode.show(state.inputIsEnabled)
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
} else {
@ -198,6 +204,14 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
fg.tvBalance.update(balanceText)
fg.tilAmountToSend.isEnabled = state.inputIsEnabled
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
fg.tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
fg.tvAmountCurrency.setTextColor(fg.getColor(textColor))
}
private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) {

View file

@ -25,7 +25,11 @@ sealed class WalletAction : Action {
}
data class LoadWallet(val allowTopUp: Boolean? = null, val blockchain: Blockchain? = null) : WalletAction() {
data class LoadWallet(
val allowToSell: Boolean? = null, val allowToBuy: Boolean? = null,
val blockchain: Blockchain? = null,
) :
WalletAction() {
data class Success(val wallet: Wallet) : WalletAction()
data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction()
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
@ -39,13 +43,16 @@ sealed class WalletAction : Action {
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet() {
constructor(walletManager: WalletManager) : this(listOf(walletManager))
}
data class AddBlockchain(val blockchain: Blockchain) : MultiWallet()
data class AddBlockchains(val blockchains: List<Blockchain>) : MultiWallet()
data class AddTokens(val tokens: List<Token>) : MultiWallet()
data class AddToken(val token: Token) : MultiWallet()
data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet()
object FindTokensInUse : MultiWallet()
data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) : MultiWallet()
data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) :
MultiWallet()
data class TokenLoaded(val amount: Amount, val token: Token) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData) : MultiWallet()
@ -69,6 +76,7 @@ sealed class WalletAction : Action {
object SetNeverToShow : Warnings()
object RemindLater : Warnings()
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
}
@ -79,7 +87,7 @@ sealed class WalletAction : Action {
}
data class LoadFiatRate(
val wallet: Wallet? = null, val currency: Currency? = null
val wallet: Wallet? = null, val currency: Currency? = null,
) : WalletAction() {
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
object Failure : WalletAction()
@ -114,6 +122,7 @@ sealed class WalletAction : Action {
object QrCode : WalletAction()
object ScanFails : WalletAction()
object SignedHashesMultiWalletDialog : WalletAction()
object ChooseTradeActionDialog : WalletAction()
}
object HideDialog : WalletAction()
@ -123,8 +132,16 @@ sealed class WalletAction : Action {
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
sealed class TradeCryptoAction : WalletAction() {
object Buy : TradeCryptoAction()
object Sell : TradeCryptoAction()
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
data class SendCrypto(
val currencyId: String,
val amount: String,
val destinationAddress: String,
val transactionId: String
) : TradeCryptoAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
@ -133,8 +150,8 @@ sealed class WalletAction : Action {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
@ -34,6 +35,7 @@ data class WalletState(
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val tradeCryptoAllowed: TradeCryptoAvailability = TradeCryptoAvailability()
) : StateType {
val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null
@ -141,6 +143,7 @@ sealed class WalletDialog: StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object ScanFailsDialog : WalletDialog()
object SignedHashesMultiWalletDialog : WalletDialog()
object ChooseTradeActionDialog : WalletDialog()
}
enum class ProgressState { Loading, Done, Error }
@ -180,10 +183,15 @@ data class Artwork(
}
}
data class TopUpState(
val allowed: Boolean = true,
val url: String? = null,
val redirectUrl: String? = null
data class TradeCryptoState(
val sellingAllowed: Boolean = false,
val buyingAllowed: Boolean = false,
)
data class TradeCryptoAvailability(
val sellingAllowed: Boolean = false,
val buyingAllowed: Boolean = false,
val availableToSell: Set<String> = TradeCryptoHelper.AVAILABLE_TO_SELL,
)
data class TwinCardsState(
@ -194,17 +202,17 @@ data class TwinCardsState(
)
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val updatingWallet: Boolean = false,
val topUpState: TopUpState = TopUpState(),
val allowToSend: Boolean = true,
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency? = null
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val updatingWallet: Boolean = false,
val tradeCryptoState: TradeCryptoState = TradeCryptoState(),
val allowToSend: Boolean = true,
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency? = null
) {
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false

View file

@ -1,53 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.tap.domain.topup.TopUpHelper
import com.tangem.tap.domain.topup.TopUpManager
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val selectedWalletData = store.state.walletState.getSelectedWalletData()
val config = store.state.globalState.configManager?.config ?: return
val addresses = selectedWalletData?.walletAddresses ?: return
if (addresses.list.isEmpty()) return
val defaultAddress = addresses.list[0].address
val currency = selectedWalletData.currency
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
val walletManager = store.state.walletState.getWalletManager(currency.token)
if (walletManager !is EthereumWalletManager) return
scope.launch {
TopUpManager().topUpTestErc20Tokens(
walletManager = walletManager, token = currency.token
)
}
} else {
val url = TopUpHelper.getUrl(
currency?.blockchain,
selectedWalletData.currencyData.currencySymbol!!,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)
.build()
customTabsIntent.launchUrl(action.context, Uri.parse(url))
}
}
}
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.topup.TopUpManager
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import timber.log.Timber
class TradeCryptoMiddleware {
fun handle(action: WalletAction.TradeCryptoAction) {
when (action) {
is WalletAction.TradeCryptoAction.Buy -> startExchange(action)
is WalletAction.TradeCryptoAction.Sell -> startExchange(action)
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
}
}
private fun startExchange(action: WalletAction.TradeCryptoAction) {
val selectedWalletData = store.state.walletState.getSelectedWalletData()
val config = store.state.globalState.configManager?.config ?: return
val addresses = selectedWalletData?.walletAddresses ?: return
if (addresses.list.isEmpty()) return
val defaultAddress = addresses.list[0].address
val currency = selectedWalletData.currency
val currencySymbol = selectedWalletData.currency?.currencySymbol ?: return
val exchangeAction = if (action is WalletAction.TradeCryptoAction.Buy) {
TradeCryptoHelper.Action.Buy
} else {
TradeCryptoHelper.Action.Sell
}
if (exchangeAction == TradeCryptoHelper.Action.Buy &&
currency is Currency.Token && currency.blockchain.isTestnet()
) {
val walletManager = store.state.walletState.getWalletManager(currency.token)
if (walletManager !is EthereumWalletManager) return
scope.launch {
TopUpManager().topUpTestErc20Tokens(
walletManager = walletManager, token = currency.token
)
}
return
}
val url = TradeCryptoHelper.getUrl(
exchangeAction,
currency?.blockchain,
currencySymbol,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
Timber.d("Moonpay $exchangeAction URL: $url")
store.dispatchOnMain(NavigationAction.OpenUrl(url))
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val walletManager =
store.state.walletState.getWalletManager(selectedWalletData.currency)
store.dispatchOnMain(PrepareSendScreen(
coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin),
coinRate = selectedWalletData.fiatRate,
walletManager = walletManager
))
store.dispatchOnMain(SendAction.SendSpecificTransaction(
sendAmount = action.amount,
destinationAddress = action.destinationAddress,
transactionId = action.transactionId
))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
}
private fun openReceiptUrl(transactionId: String) {
store.dispatchOnMain(NavigationAction.PopBackTo())
val url = TradeCryptoHelper.getSellCryptoReceiptUrl(transactionId)
store.dispatchOnMain(NavigationAction.OpenUrl(url))
}
}

View file

@ -28,7 +28,7 @@ import org.rekotlin.Action
import org.rekotlin.Middleware
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
private val twinsMiddleware = TwinsMiddleware()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
@ -39,7 +39,7 @@ class WalletMiddleware {
val globalState = state()?.globalState
val walletState = state()?.walletState
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(action)
is WalletAction.TwinsAction -> twinsMiddleware.handle(action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet ->

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
@ -38,8 +37,12 @@ class MultiWalletReducer {
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
currency = Currency.Blockchain(blockchain)
currency = Currency.Blockchain(blockchain),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(blockchain.currency),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}
@ -63,8 +66,12 @@ class MultiWalletReducer {
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
currency = Currency.Blockchain(action.blockchain)
currency = Currency.Blockchain(action.blockchain),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(action.blockchain.currency),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
val newState = state.copy(wallets = state.replaceWalletInWallets(walletData))
if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) {
@ -165,7 +172,11 @@ fun Token.toWallet(state: WalletState): WalletData? {
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
currency = Currency.Token(this)
currency = Currency.Token(this),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(this.symbol),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}

View file

@ -50,7 +50,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
topUpState = TopUpState(false)
)
)
)
@ -78,7 +77,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
wallets = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
topUpState = TopUpState(false)
)
)
)
@ -95,6 +93,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadWallet -> {
if (action.blockchain == null) {
val wallets = newState.wallets.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,
@ -102,15 +101,22 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(
allowed = action.allowTopUp
?: wallet.topUpState.allowed
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
)
)
}
val tradeCryptoAllowed = TradeCryptoAvailability(
sellingAllowed = action.allowToSell ?: false,
buyingAllowed = action.allowToBuy ?: false
)
newState = newState.copy(
state = ProgressState.Loading,
wallets = wallets
wallets = wallets,
tradeCryptoAllowed = tradeCryptoAllowed
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
@ -126,8 +132,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(
allowed = action.allowTopUp ?: wallet.topUpState.allowed
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
)
)
}
@ -172,7 +181,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
status = BalanceStatus.Unreachable,
errorMessage = message
),
topUpState = TopUpState(false)
)
val tokenWallets = action.wallet.getTokens()
.mapNotNull { newState.getWalletData(it) }
@ -242,6 +250,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.ShowDialog.SignedHashesMultiWalletDialog -> {
newState = newState.copy(walletDialog = WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.ShowDialog.ChooseTradeActionDialog -> {
newState = newState.copy(walletDialog = WalletDialog.ChooseTradeActionDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
@ -251,7 +262,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.TopUpAction -> return newState
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)

View file

@ -86,11 +86,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
btn_share.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray)
)
}
btn_top_up.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) }
btn_sell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
}
override fun newState(state: WalletState) {
@ -131,8 +129,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
srl_wallet_details.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
selectedWallet.topUpState.allowed,
selectedWallet.currency?.blockchain
allowToBuy = selectedWallet.tradeCryptoState.buyingAllowed,
allowToSell = selectedWallet.tradeCryptoState.sellingAllowed,
blockchain = selectedWallet.currency?.blockchain
))
}
}
@ -140,6 +139,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
srl_wallet_details.isRefreshing = false
}
btn_top_up.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed
btn_sell.show(selectedWallet.tradeCryptoState.sellingAllowed)
}
private fun handleCurrencyIcon(wallet: WalletData) {

View file

@ -0,0 +1,32 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_wallet_trade.*
class ChooseTradeActionDialog(context: Context) : BottomSheetDialog(context) {
init {
this.setContentView(R.layout.dialog_wallet_trade)
}
override fun show() {
super.show()
this.setOnDismissListener {
store.dispatch(WalletAction.HideDialog)
}
dialog_btn_buy.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy)
dismiss()
}
dialog_btn_sell.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Sell)
dismiss()
}
}
}

View file

@ -16,10 +16,7 @@ import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.features.wallet.ui.dialogs.*
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
@ -124,12 +121,17 @@ class SingleWalletView : WalletView {
setupButtonsType(state, fragment)
val btnConfirm = if (state.topUpState.allowed) {
val btnConfirm = if (state.tradeCryptoState.sellingAllowed ||
state.tradeCryptoState.buyingAllowed
) {
btn_confirm_short
} else {
btn_confirm_long
}
val btnScan = if (state.topUpState.allowed) {
val btnScan = if (state.tradeCryptoState.sellingAllowed ||
state.tradeCryptoState.buyingAllowed
) {
btn_scan_short
} else {
btn_scan_long
@ -149,14 +151,26 @@ class SingleWalletView : WalletView {
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray)
)
tradeCryptoAction(state.tradeCryptoState)
}
}
private fun tradeCryptoAction(tradeCryptoState: TradeCryptoState) {
val allowedToBuy = tradeCryptoState.buyingAllowed
val allowedToSell = tradeCryptoState.sellingAllowed
val action = when {
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy
!allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell
allowedToBuy && allowedToSell -> WalletAction.ShowDialog.ChooseTradeActionDialog
else -> null
}
if (action != null) store.dispatch(action)
}
private fun setupButtonsType(state: WalletData, fragment: WalletFragment) = with(fragment) {
if (state.topUpState.allowed) {
if (state.tradeCryptoState.sellingAllowed ||
state.tradeCryptoState.buyingAllowed
) {
l_buttons_long.hide()
l_buttons_short.show()
} else {
@ -244,6 +258,9 @@ class SingleWalletView : WalletView {
dialog = SignedHashesWarningDialog.create(context).apply { show() }
}
}
is WalletDialog.ChooseTradeActionDialog -> {
if (dialog == null) dialog = ChooseTradeActionDialog(context).apply { show() }
}
null -> {
dialog?.dismiss()
dialog = null

View file

@ -0,0 +1,16 @@
package com.tangem.tap.network.moonpay
import retrofit2.http.GET
import retrofit2.http.Query
interface MoonpayApi {
@GET( MOOONPAY_IP_ADDRESS_REQUEST_URL)
suspend fun getUserStatus(
@Query("apiKey") moonpayApiKey: String
): MoonPayUserStatus
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/v4/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "ip_address/"
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.network.moonpay
import com.squareup.moshi.JsonClass
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.performRequest
import com.tangem.tap.network.createRetrofitInstance
class MoonpayService {
private val moonpayApi: MoonpayApi by lazy {
createRetrofitInstance(MoonpayApi.MOOONPAY_BASE_URL)
.create(MoonpayApi::class.java)
}
suspend fun getUserStatus(moonpayApiKey: String): Result<MoonPayUserStatus> {
return performRequest { moonpayApi.getUserStatus(moonpayApiKey) }
}
}
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean
)

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="20">
<group
android:translateX="4"
android:translateY="0">
<path
android:fillColor="#ffffff"
android:pathData="M11.4765,5.7026L6.7623,10.0101C6.4406,10.2874 6.412,10.8356 6.692,11.1572C6.972,11.4788 7.5063,11.5112 7.8099,11.2131L11.2146,8.1003L11.2146,18.6923C11.2146,19.1384 11.5664,19.5 12.0003,19.5C12.4342,19.5 12.7859,19.1384 12.7859,18.6923L12.7859,8.1003L16.1906,11.2131C16.4942,11.5112 17.0275,11.4709 17.3076,11.1493C17.5876,10.8277 17.5599,10.2874 17.2382,10.0101L12.524,5.7026C12.1429,5.4214 11.8008,5.4438 11.4765,5.7026L11.4765,5.7026Z" />
</group>
<group
android:pivotX="9"
android:pivotY="12.5"
android:rotation="180">
<path
android:fillColor="#ffffff"
android:pathData="M11.4765,5.7026L6.7623,10.0101C6.4406,10.2874 6.412,10.8356 6.692,11.1572C6.972,11.4788 7.5063,11.5112 7.8099,11.2131L11.2146,8.1003L11.2146,18.6923C11.2146,19.1384 11.5664,19.5 12.0003,19.5C12.4342,19.5 12.7859,19.1384 12.7859,18.6923L12.7859,8.1003L16.1906,11.2131C16.4942,11.5112 17.0275,11.4709 17.3076,11.1493C17.5876,10.8277 17.5599,10.2874 17.2382,10.0101L12.524,5.7026C12.1429,5.4214 11.8008,5.4438 11.4765,5.7026L11.4765,5.7026Z" />
</group>
</vector>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:padding="16dp"
android:text="@string/wallet_dialog_choose_trade_action"
android:textColor="@color/darkGray2"
android:textSize="14sp" />
<TextView
android:id="@+id/dialog_btn_buy"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:drawablePadding="32dp"
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_buy"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_up" />
<TextView
android:id="@+id/dialog_btn_sell"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:drawablePadding="32dp"
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_sell"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_down" />
</LinearLayout>

View file

@ -11,7 +11,7 @@
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_close"
app:title="@string/wallet_button_topup" />
app:title="@string/wallet_button_buy" />
<WebView
android:id="@+id/web_view"

View file

@ -13,7 +13,7 @@
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:background="@color/toolbarColor"
android:fitsSystemWindows="true"
app:liftOnScroll="true">

View file

@ -83,12 +83,12 @@
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/barrier_currency_title" />
<include
@ -114,13 +114,13 @@
app:layout_constraintTop_toBottomOf="@+id/l_wallet_details">
<TextView
android:padding="16dp"
android:id="@+id/tv_pending_transaction_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:padding="16dp"
android:text="@string/wallet_pending_transaction_warning"
android:textColor="@android:color/white" />
android:textColor="@android:color/white"
android:textSize="14sp" />
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Barrier
@ -136,24 +136,46 @@
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="33dp"
android:layout_marginEnd="4dp"
android:text="@string/wallet_button_topup"
android:layout_marginBottom="33dp"
android:text="@string/wallet_button_buy"
app:icon="@drawable/ic_arrow_up"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintEnd_toStartOf="@+id/btn_sell"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintVertical_bias="1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:barrierDirection="end"
app:constraint_referenced_ids="btn_top_up, btn_sell" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="btn_sell, btn_confirm" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sell"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="4dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="33dp"
android:text="@string/wallet_button_sell"
app:icon="@drawable/ic_arrow_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.5" />
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/btn_top_up"
app:layout_constraintEnd_toStartOf="@id/btn_confirm"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_confirm"
@ -164,11 +186,12 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"
android:text="@string/wallet_button_send"
app:layout_constraintStart_toEndOf="@id/btn_sell"
app:icon="@drawable/ic_send"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -32,6 +32,7 @@
android:id="@+id/etAddressOrPayId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:ellipsize="middle"
android:inputType="textNoSuggestions"
android:paddingStart="0dp"
@ -150,6 +151,7 @@
android:id="@+id/etMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:ellipsize="middle"
android:paddingStart="0dp"
android:paddingEnd="0dp"
@ -180,6 +182,7 @@
android:id="@+id/etDestinationTag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:ellipsize="end"
android:inputType="number"
android:paddingStart="0dp"

View file

@ -51,6 +51,7 @@
android:id="@+id/etAmountToSend"
android:layout_width="match_parent"
android:layout_height="82dp"
android:background="@color/backgroundLightGray"
android:fontFamily="sans-serif-light"
android:inputType="numberDecimal"
android:paddingStart="0dp"

View file

@ -28,10 +28,10 @@
android:layout_marginStart="8dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="33dp"
android:drawableTop="@drawable/ic_arrow_up_short_btn"
android:drawableTop="@drawable/ic_arrows_up_down_short_btn"
android:gravity="bottom|center_horizontal"
android:paddingTop="1dp"
android:text="@string/wallet_button_topup"
android:text="@string/wallet_button_trade"
app:drawableTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_confirm_short"

View file

@ -5,6 +5,8 @@
<color name="colorSecondary">#0022D4</color>
<color name="accent">#1ACE80</color>
<color name="toolbarColor">@color/backgroundLightGray</color>
<color name="tapButtonColor">@color/accent</color>
<color name="tapButtonDisabled">#661ACE80</color>
<color name="tapButtonColorBlack">@color/darkGray6</color>

View file

@ -1,5 +1,7 @@
<resources>
<string name="wallet_button_topup" translatable="false">Top Up</string>
<string name="wallet_button_buy" translatable="false">Buy</string>
<string name="wallet_button_sell" translatable="false">Sell</string>
<string name="wallet_button_trade" translatable="false">Trade</string>
<string name="wallet_address_chip_default" translatable="false">Default</string>
<string name="wallet_address_chip_compatibility" translatable="false">Compatibility</string>
<string name="wallet_address_chip_legacy" translatable="false">Legacy</string>
@ -161,4 +163,7 @@ this wallet.
<string name="wallet_connect_error_timeout" translatable="false">Failed to establish WalletConnect session: timeout error. Please, try again later.</string>
<string name="warning_testnet_card_message" translatable="false">This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes.</string>
<string name="wallet_dialog_choose_trade_action" translatable="false">Do you want to buy or sell crypto?</string>
</resources>