Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-29 19:41:26 +04:00
parent 1474d83756
commit 04d22aa092
26 changed files with 201 additions and 55 deletions

View file

@ -2,6 +2,7 @@ plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.google.dagger.hilt.android")
@ -102,6 +103,7 @@ android {
}
repositories {
mavenCentral()
maven(url = "https://jitpack.io")
}
@ -199,6 +201,7 @@ dependencies {
implementation(Library.googlePlayServicesWallet)
implementation(Library.composeShimmer)
implementation(Library.mviCoreWatcher)
implementation(Library.kotlinSerialization)
/** Testing libraries */
testImplementation(Test.junit)

View file

@ -133,7 +133,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context, state.dialog)
is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog)
is WalletDialog.SignedHashesMultiWalletDialog -> SignedHashesWarningDialog.create(context)
is WalletDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.extensions
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
@ -7,6 +8,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
@ -37,10 +39,12 @@ import timber.log.Timber
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
bundle: Bundle? = null,
fgShareTransition: FragmentShareTransition? = null,
) {
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = fragmentFactory(screen)
fragment.arguments = bundle
fgShareTransition?.apply {
fragment.sharedElementEnterTransition = enterTransitionSet
fragment.sharedElementReturnTransition = exitTransitionSet
@ -117,6 +121,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Swap -> SwapFragment()
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux.navigation
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.rekotlin.Action
import java.lang.ref.WeakReference
@ -10,6 +11,7 @@ sealed class NavigationAction : Action {
val screen: AppScreen,
val fragmentShareTransition: FragmentShareTransition? = null,
val addToBackstack: Boolean = true,
val bundle: Bundle? = null,
) : NavigationAction()
data class PopBackTo(

View file

@ -23,6 +23,7 @@ enum class AppScreen(
WalletConnectSessions,
QrScan,
ReferralProgram,
Swap,
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),

View file

@ -161,7 +161,12 @@ sealed class WalletAction : Action {
) : DialogAction()
object SignedHashesMultiWalletDialog : DialogAction()
object ChooseTradeActionDialog : DialogAction()
data class ChooseTradeActionDialog(
val buyAllowed: Boolean,
val sellAllowed: Boolean,
val swapAllowed: Boolean,
) : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
data class RussianCardholdersWarningDialog(
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null,
@ -190,6 +195,8 @@ sealed class WalletAction : Action {
val destinationAddress: String,
val transactionId: String,
) : TradeCryptoAction()
object Swap : TradeCryptoAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()

View file

@ -329,6 +329,9 @@ data class WalletData(
val isAvailableToSell: Boolean
get() = store.state.globalState.exchangeManager.availableForSell(currency)
val isAvailableToSwap: Boolean
get() = currency.blockchain.isEvm() && currency.coinId != null
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false
return listOfAddresses.size > 1

View file

@ -1,7 +1,10 @@
package com.tangem.tap.features.wallet.redux.middlewares
import androidx.core.os.bundleOf
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
@ -11,6 +14,7 @@ import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.send.redux.PrepareSendScreen
@ -22,6 +26,9 @@ import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import com.tangem.feature.swap.domain.models.data.Currency as SwapCurrency
class TradeCryptoMiddleware {
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
@ -32,6 +39,7 @@ class TradeCryptoMiddleware {
is WalletAction.TradeCryptoAction.Sell -> proceedSellAction()
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is WalletAction.TradeCryptoAction.Swap -> openSwap()
}
}
@ -136,4 +144,33 @@ class TradeCryptoMiddleware {
transactionId = transactionId,
)?.let { store.dispatchOpenUrl(it) }
}
private fun openSwap() {
val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()
val bundle = bundleOf(SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency))
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
}
private fun Currency.toSwapCurrency(): SwapCurrency {
return when (this) {
is Currency.Blockchain -> {
SwapCurrency.NativeToken(
id = blockchain.id,
name = this.currencyName,
symbol = this.currencySymbol,
networkId = this.blockchain.toNetworkId(),
logoUrl = getIconUrl(this.blockchain.toNetworkId()),
)
}
is Currency.Token -> SwapCurrency.NonNativeToken(
id = this.token.id ?: "",
name = this.currencyName,
symbol = this.currencySymbol,
networkId = this.blockchain.toNetworkId(),
logoUrl = getIconUrl(this.token.id ?: ""),
contractAddress = this.token.contractAddress,
decimalCount = decimals,
)
}
}
}

View file

@ -20,7 +20,13 @@ class WalletDialogsMiddleware {
store.state.walletState.selectedWalletData?.let {
Analytics.send(Token.ButtonExchange(AnalyticsParam.CurrencyType.Currency(it.currency)))
}
store.dispatchDialogShow(WalletDialog.ChooseTradeActionDialog)
store.dispatchDialogShow(
WalletDialog.ChooseTradeActionDialog(
buyAllowed = action.buyAllowed,
sellAllowed = action.sellAllowed,
swapAllowed = action.swapAllowed,
),
)
}
is WalletAction.DialogAction.QrCode -> {
store.dispatchDialogShow(

View file

@ -9,7 +9,12 @@ import com.tangem.wallet.R
sealed interface WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog
object SignedHashesMultiWalletDialog : WalletDialog
object ChooseTradeActionDialog : WalletDialog
data class ChooseTradeActionDialog(
val buyAllowed: Boolean,
val sellAllowed: Boolean,
val swapAllowed: Boolean,
) : WalletDialog
data class CurrencySelectionDialog(
val currenciesList: List<FiatCurrency>,
val currentAppCurrency: FiatCurrency,
@ -17,7 +22,7 @@ sealed interface WalletDialog : StateDialog {
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit
val onOk: () -> Unit,
) : WalletDialog {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
@ -26,7 +31,7 @@ sealed interface WalletDialog : StateDialog {
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String
val currencySymbol: String,
) : WalletDialog {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title

View file

@ -165,15 +165,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun setupButtons() = with(binding) {
rowButtons.onBuyClick = {
store.dispatch(WalletAction.TradeCryptoAction.Buy())
}
rowButtons.onSellClick = {
store.dispatch(WalletAction.TradeCryptoAction.Sell)
}
rowButtons.onTradeClick = {
store.dispatch(WalletAction.DialogAction.ChooseTradeActionDialog)
}
rowButtons.onSendClick = {
store.dispatch(WalletAction.Send())
}
@ -273,6 +264,15 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
sellAllowed = selectedWallet.isAvailableToSell,
sendAllowed = selectedWallet.mainButton.enabled,
)
rowButtons.onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy,
sellAllowed = selectedWallet.isAvailableToSell,
swapAllowed = selectedWallet.isAvailableToSwap,
),
)
}
}
private fun handleWarnings(selectedWallet: WalletData) = with(binding) {

View file

@ -4,11 +4,16 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogWalletTradeBinding
class ChooseTradeActionBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
class ChooseTradeActionBottomSheetDialog(
context: Context,
private val dialogData: WalletDialog.ChooseTradeActionDialog,
) : BottomSheetDialog(context) {
var binding: DialogWalletTradeBinding? = null
@ -26,13 +31,26 @@ class ChooseTradeActionBottomSheetDialog(context: Context) : BottomSheetDialog(c
store.dispatch(WalletAction.DialogAction.Hide)
}
binding!!.dialogBtnBuy.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Buy())
}
binding!!.dialogBtnSell.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Sell)
binding?.let {
with(it)
{
dialogBtnBuy.show(dialogData.buyAllowed)
dialogBtnSell.show(dialogData.sellAllowed)
dialogBtnSwap.show(dialogData.swapAllowed)
dialogBtnBuy.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Buy())
}
dialogBtnSell.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Sell)
}
dialogBtnSwap.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Swap)
}
}
}
}
}

View file

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.view.isVisible
import com.tangem.tap.common.extensions.hide
import com.tangem.wallet.databinding.ViewWalletDetailsButtonsRowBinding
internal class WalletDetailsButtonsRow @JvmOverloads constructor(
@ -39,11 +40,9 @@ internal class WalletDetailsButtonsRow @JvmOverloads constructor(
sendAllowed: Boolean,
) = with(binding) {
containerExchangeButtons.isVisible = exchangeServiceFeatureOn
btnBuy.isVisible = (buyAllowed && !sellAllowed) || (!buyAllowed && !sellAllowed)
btnBuy.isEnabled = buyAllowed
btnSell.isVisible = !buyAllowed && sellAllowed
btnTrade.isVisible = buyAllowed && sellAllowed
btnBuy.hide()
btnSell.hide()
btnTrade.isVisible = exchangeServiceFeatureOn
btnSend.isEnabled = sendAllowed
}
}

View file

@ -134,9 +134,15 @@ class SingleWalletView : WalletView() {
sendAllowed = walletData.mainButton.enabled,
)
rowButtons.onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
rowButtons.onSendClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
rowButtons.onTradeClick = { store.dispatch(WalletAction.DialogAction.ChooseTradeActionDialog) }
rowButtons.onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = walletData.isAvailableToBuy,
sellAllowed = walletData.isAvailableToSell,
swapAllowed = walletData.isAvailableToSwap,
),
)
}
rowButtons.onSendClick = {
when (walletData.mainButton) {

View file

@ -27,7 +27,7 @@
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_up" />
app:drawableStartCompat="@drawable/ic_arrow_up_24" />
<TextView
android:id="@+id/dialog_btn_sell"
@ -42,6 +42,21 @@
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_down" />
app:drawableStartCompat="@drawable/ic_arrow_down_24" />
<TextView
android:id="@+id/dialog_btn_swap"
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/swapping_swap"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_exchange_vertical_24" />
</LinearLayout>

View file

@ -16,6 +16,7 @@ buildscript {
plugins {
id("com.google.dagger.hilt.android") version "2.44" apply false
kotlin("plugin.serialization") version Versions.kotlin apply false
}
allprojects {

View file

@ -67,6 +67,7 @@ object Library {
const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner
const val zxingQrCore = "com.google.zxing:core:" + Versions.zxingQrCode
const val mviCoreWatcher = "com.github.badoo.mvicore:mvicore-diff:" + Versions.mviCore
const val kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:" + Versions.kotlinSerialization
}
object Tangem {

View file

@ -56,6 +56,7 @@ object Versions {
const val zxingQrBarcodeScanner = "1.9.8"
const val zxingQrCode = "3.3.3"
const val mviCore = "1.3.1"
const val kotlinSerialization = "1.4.1"
// endregion Other libraries
// region Tangem

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M11.08,4.08L13.08,4.08V16.08L18.58,10.58L20,12L12.08,19.92L4.16,12L5.58,10.58L11.08,16.08L11.08,4.08Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13.08,19.92H11.08L11.08,7.92L5.58,13.42L4.16,12L12.08,4.08L20,12L18.58,13.42L13.08,7.92V19.92Z" />
</vector>

View file

@ -2,10 +2,10 @@ plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
kotlin("kapt")
kotlin("plugin.serialization")
}
dependencies {
/** Libs */
implementation(project(":libs:crypto"))
implementation(project(":core:utils"))
@ -13,6 +13,9 @@ dependencies {
/** DI */
implementation(Library.hiltCore)
kapt(Library.hiltKapt)
/** Other Libraries **/
implementation(Library.kotlinSerialization)
}
java {

View file

@ -1,21 +1,25 @@
package com.tangem.feature.swap.domain.models.data
sealed class Currency(
open val id: String,
open val name: String,
open val symbol: String,
open val networkId: String,
open val logoUrl: String,
) {
import kotlinx.serialization.Serializable
@Serializable
sealed class Currency {
abstract val id: String
abstract val name: String
abstract val symbol: String
abstract val networkId: String
abstract val logoUrl: String
@Serializable
data class NativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
override val logoUrl: String,
) : Currency(id, name, symbol, networkId, logoUrl)
) : Currency()
@Serializable
data class NonNativeToken(
override val id: String,
override val name: String,
@ -24,5 +28,5 @@ sealed class Currency(
override val logoUrl: String,
val contractAddress: String,
val decimalCount: Int,
) : Currency(id, name, symbol, networkId, logoUrl)
) : Currency()
}

View file

@ -2,6 +2,7 @@ plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization")
id("com.google.dagger.hilt.android")
}
@ -68,6 +69,7 @@ dependencies {
/** Other libraries */
implementation(Library.composeShimmer)
implementation(Library.kotlinSerialization)
/** DI */
implementation(Library.hilt)

View file

@ -57,4 +57,8 @@ class SwapFragment : Fragment() {
}
}
}
companion object {
const val CURRENCY_BUNDLE_KEY = "swap_currency"
}
}

View file

@ -31,7 +31,7 @@ import com.tangem.feature.swap.presentation.R
@Composable
fun SwapPermissionBottomSheetContent(
data: SwapPermissionStateHolder,
data: SwapPermissionStateHolder?,
onCancel: () -> Unit,
) {
Column(
@ -54,7 +54,10 @@ fun SwapPermissionBottomSheetContent(
SpacerH10()
Text(
text = stringResource(id = R.string.swapping_permission_subheader, data.currency),
text = stringResource(
id = R.string.swapping_permission_subheader,
data?.currency ?: "",
),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
@ -64,11 +67,11 @@ fun SwapPermissionBottomSheetContent(
SpacerH16()
ApprovalBottomSheetInfo(
currency = data.currency,
amount = data.amount,
walletAddress = data.walletAddress,
spenderAddress = data.spenderAddress,
fee = data.fee,
currency = data?.currency ?: "",
amount = data?.amount ?: "",
walletAddress = data?.walletAddress ?: "",
spenderAddress = data?.spenderAddress ?: "",
fee = data?.fee ?: "",
)
SpacerH28()
@ -77,7 +80,7 @@ fun SwapPermissionBottomSheetContent(
text = stringResource(id = R.string.swapping_permission_buttons_approve),
icon = painterResource(id = R.drawable.ic_tangem_24),
modifier = Modifier.fillMaxWidth(),
onClick = data.approveButton.onClick,
onClick = { data?.approveButton?.onClick?.invoke() },
)
SpacerH12()
@ -86,7 +89,7 @@ fun SwapPermissionBottomSheetContent(
text = stringResource(id = R.string.common_cancel),
modifier = Modifier.fillMaxWidth(),
) {
data.cancelButton.onClick()
data?.cancelButton?.onClick?.invoke()
onCancel()
}

View file

@ -33,12 +33,10 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
)
},
sheetContent = {
if (stateHolder.permissionState != null) {
SwapPermissionBottomSheetContent(
data = stateHolder.permissionState,
onCancel = { coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() } },
)
}
},
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
@ -52,7 +50,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
state = stateHolder,
onPermissionWarningClick = {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed
&& stateHolder.permissionState != null
) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()