Updated on 2026-08-14

This commit is contained in:
Tangem 2021-11-03 15:08:17 +00:00
commit 1db7ea1f49
17 changed files with 138 additions and 81 deletions

View file

@ -12,7 +12,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
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 com.tangem.tap.network.moonpay.MoonpayStatus
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -72,7 +72,7 @@ sealed class GlobalAction : Action {
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object GetMoonPayUserStatus : GlobalAction() {
data class Success(val moonPayUserStatus: MoonPayUserStatus) : GlobalAction()
object GetMoonPayStatus : GlobalAction() {
data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction()
}
}

View file

@ -74,14 +74,14 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
is GlobalAction.GetMoonPayStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
scope.launch {
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
val moonpayStatus = MoonpayService().getMoonpayStatus(apiKey)
if (moonpayStatus is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
GlobalAction.GetMoonPayStatus.Success(moonpayStatus.data)
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
@ -66,8 +67,19 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayUserStatus.Success -> {
globalState.copy(moonPayUserStatus = action.moonPayUserStatus)
is GlobalAction.GetMoonPayStatus.Success -> {
val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false
val moonpayStatus = if (fiatExchangeIsEnabled) {
action.moonPayStatus
} else {
MoonpayStatus(
isBuyAllowed = false,
isSellAllowed = false,
availableToBuy = emptyList(),
availableToSell = emptyList()
)
}
globalState.copy(moonpayStatus = moonpayStatus)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)

View file

@ -10,7 +10,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import com.tangem.tap.network.moonpay.MoonpayStatus
import org.rekotlin.StateType
data class GlobalState(
@ -26,7 +26,7 @@ data class GlobalState(
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
val moonpayStatus: MoonpayStatus? = null,
val resources: AndroidResources = AndroidResources(),
) : StateType

View file

@ -145,11 +145,8 @@ class TapWalletManager {
loadMultiWalletData(data.card, blockchain, null)
}
}
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}
@ -212,11 +209,8 @@ class TapWalletManager {
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}

View file

@ -38,15 +38,6 @@ class TradeCryptoHelper {
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?,

View file

@ -37,7 +37,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.GetMoonPayUserStatus)
store.dispatch(GlobalAction.GetMoonPayStatus)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
}
is HomeAction.ShouldScanCardOnResume -> {

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.store
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
@ -26,10 +28,16 @@ data class OnboardingNoteState(
get() = steps.indexOf(currentStep)
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonPayUserStatus?.isBuyAllowed ?: false
store.state.globalState.moonpayStatus?.canBuy(walletBalance) ?: false
}
}
fun MoonpayStatus.canBuy(walletBalance: OnboardingWalletBalance): Boolean {
if (walletBalance.currency.blockchain == Blockchain.Unknown) return false
return isBuyAllowed && availableToBuy.contains(walletBalance.currency.currencySymbol)
}
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}

View file

@ -5,6 +5,7 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.note.redux.canBuy
import com.tangem.tap.store
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
@ -56,7 +57,7 @@ data class TwinCardsState(
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonPayUserStatus?.isBuyAllowed ?: false
store.state.globalState.moonpayStatus?.canBuy(walletBalance) ?: false
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -25,7 +26,7 @@ sealed class WalletAction : Action {
data class LoadWallet(
val allowToSell: Boolean? = null, val allowToBuy: Boolean? = null,
val moonpayStatus: MoonpayStatus? = null,
val blockchain: Blockchain? = null,
) :
WalletAction() {

View file

@ -11,7 +11,6 @@ import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
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.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
@ -38,7 +37,6 @@ data class WalletState(
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val tradeCryptoAllowed: TradeCryptoAvailability = TradeCryptoAvailability()
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -221,12 +219,6 @@ data class TradeCryptoState(
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 WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,

View file

@ -54,7 +54,11 @@ class MultiWalletMiddleware {
}
}
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain)))
store.dispatch(WalletAction.LoadWallet(blockchain = action.blockchain))
store.dispatch(WalletAction.LoadWallet(
moonpayStatus = globalState?.moonpayStatus,
blockchain = action.blockchain
)
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = globalState?.scanResponse?.card?.cardId

View file

@ -38,11 +38,6 @@ class MultiWalletReducer {
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(blockchain),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(blockchain.currency),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}
@ -67,11 +62,6 @@ class MultiWalletReducer {
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
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) {
@ -173,10 +163,5 @@ fun Token.toWallet(state: WalletState): WalletData? {
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Token(this),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(this.symbol),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}

View file

@ -96,21 +96,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
sellingAllowed = action.moonpayStatus?.availableToSell?.contains(
wallet.currencyData.currencySymbol
) ?: wallet.tradeCryptoState.sellingAllowed,
buyingAllowed = action.moonpayStatus?.availableToBuy?.contains(
wallet.currencyData.currencySymbol
) ?: wallet.tradeCryptoState.buyingAllowed
)
)
}
val tradeCryptoAllowed = TradeCryptoAvailability(
sellingAllowed = action.allowToSell ?: false,
buyingAllowed = action.allowToBuy ?: false
)
newState = newState.copy(
state = ProgressState.Loading,
wallets = wallets,
tradeCryptoAllowed = tradeCryptoAllowed
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
@ -127,10 +124,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
sellingAllowed = action.moonpayStatus?.availableToSell?.contains(
wallet.currencyData.currencySymbol
) ?: wallet.tradeCryptoState.sellingAllowed,
buyingAllowed = action.moonpayStatus?.availableToBuy?.contains(
wallet.currencyData.currencySymbol
) ?: wallet.tradeCryptoState.buyingAllowed
)
)
}

View file

@ -127,8 +127,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
srl_wallet_details.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
allowToBuy = selectedWallet.tradeCryptoState.buyingAllowed,
allowToSell = selectedWallet.tradeCryptoState.sellingAllowed,
blockchain = selectedWallet.currency?.blockchain
))
}

View file

@ -4,13 +4,20 @@ import retrofit2.http.GET
import retrofit2.http.Query
interface MoonpayApi {
@GET( MOOONPAY_IP_ADDRESS_REQUEST_URL)
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
suspend fun getUserStatus(
@Query("apiKey") moonpayApiKey: String
@Query("apiKey") moonpayApiKey: String,
): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
suspend fun getCurrencies(
@Query("apiKey") moonpayApiKey: String,
): List<MoonpayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/v4/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "ip_address/"
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}

View file

@ -1,9 +1,12 @@
package com.tangem.tap.network.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.common.services.retryIO
import com.tangem.tap.network.createRetrofitInstance
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class MoonpayService {
@ -12,13 +15,75 @@ class MoonpayService {
.create(MoonpayApi::class.java)
}
suspend fun getUserStatus(moonpayApiKey: String): Result<MoonPayUserStatus> {
return performRequest { moonpayApi.getUserStatus(moonpayApiKey) }
suspend fun getMoonpayStatus(moonpayApiKey: String): Result<MoonpayStatus> {
return try {
coroutineScope {
val userStatusDeferred =
retryIO { async { moonpayApi.getUserStatus(moonpayApiKey) } }
val currenciesDeferred =
retryIO { async { moonpayApi.getCurrencies(moonpayApiKey) } }
val userStatus = userStatusDeferred.await()
val currenciesToBuy = mutableListOf<String>()
val currenciesToSell = mutableListOf<String>()
currenciesDeferred.await().forEach { currencyStatus ->
if (currencyStatus.type != "crypto" || currencyStatus.isSuspended ||
!currencyStatus.supportsLiveMode
) {
return@forEach
}
if (userStatus.countryCode == "USA") {
if (!currencyStatus.isSupportedInUS) return@forEach
if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
}
val currencyCode = currencyStatus.code.uppercase()
currenciesToBuy.add(currencyCode)
if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
}
Result.Success(MoonpayStatus(
isBuyAllowed = userStatus.isBuyAllowed,
isSellAllowed = userStatus.isSellAllowed,
availableToBuy = if (userStatus.isBuyAllowed) currenciesToBuy else emptyList(),
availableToSell = if (userStatus.isSellAllowed) currenciesToSell else emptyList()
))
}
} catch (error: Error) {
Result.Failure(error)
}
}
}
data class MoonpayStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
val availableToBuy: List<String>,
val availableToSell: List<String>,
)
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@Json(name = "alpha3")
val countryCode: String,
@Json(name = "state")
val stateCode: String,
)
@JsonClass(generateAdapter = true)
data class MoonpayCurrencies(
val type: String,
val code: String,
val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
)