diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index f614b976..f0644848 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -355,6 +355,8 @@
DOT ↔ HEZ Bridge
+ HEZ ⇄ DOT
+ USDT ⇄ USDT.p
You send
You receive (estimated)
Exchange rate
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
index a694cbcc..0c883260 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
@@ -2,11 +2,11 @@ package io.novafoundation.nova.feature_assets.presentation.bridge
import android.text.Editable
import android.text.TextWatcher
-import android.view.View
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
+import io.novafoundation.nova.common.utils.setVisible
+import io.novafoundation.nova.common.view.AlertView
import io.novafoundation.nova.common.view.setState
-import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.databinding.FragmentBridgeBinding
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
@@ -18,26 +18,29 @@ class BridgeFragment : BaseFragment() {
override fun initViews() {
binder.bridgeToolbar.setHomeButtonListener { viewModel.backClicked() }
- // Pair selector
- binder.bridgePairDotHez.setOnClickListener {
- viewModel.setPair(BridgePair.DOT_HEZ)
+ binder.bridgeToCard.setEditable(false)
+
+ // Tapping the "from" card opens the pair picker - replaces the old segmented pair buttons
+ binder.bridgeFromCard.setCardClickListener {
+ val options = viewModel.pairOptions.value.orEmpty()
+ if (options.isNotEmpty()) {
+ BridgePairListBottomSheet(requireContext(), options) { selected ->
+ viewModel.setPair(selected.pair)
+ }.show()
+ }
}
- binder.bridgePairUsdt.setOnClickListener {
- viewModel.setPair(BridgePair.USDT)
- }
-
- // Direction toggle
- binder.bridgeDirectionLeft.setOnClickListener {
- viewModel.setDirectionLeft()
- }
-
- binder.bridgeDirectionRight.setOnClickListener {
- viewModel.setDirectionRight()
+ // One-tap direction flip - replaces the old segmented direction buttons
+ binder.bridgeFlipButton.setOnClickListener {
+ when (viewModel.direction.value) {
+ BridgeDirection.DOT_TO_HEZ, BridgeDirection.USDT_TO_WUSDT -> viewModel.setDirectionRight()
+ BridgeDirection.HEZ_TO_DOT, BridgeDirection.WUSDT_TO_USDT -> viewModel.setDirectionLeft()
+ null -> Unit
+ }
}
// Amount input
- binder.bridgeFromAmount.addTextChangedListener(object : TextWatcher {
+ binder.bridgeFromCard.amountInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
@@ -63,16 +66,16 @@ class BridgeFragment : BaseFragment() {
}
override fun subscribe(viewModel: BridgeViewModel) {
- viewModel.pair.observe { pair ->
- updatePairUI(pair)
+ viewModel.fromCard.observe { model ->
+ binder.bridgeFromCard.setModel(model)
}
- viewModel.direction.observe { direction ->
- updateDirectionUI(direction)
+ viewModel.toCard.observe { model ->
+ binder.bridgeToCard.setModel(model)
}
viewModel.outputAmount.observe { output ->
- binder.bridgeToAmount.text = output
+ binder.bridgeToCard.setAmountText(output)
}
viewModel.exchangeRateText.observe { rate ->
@@ -88,80 +91,18 @@ class BridgeFragment : BaseFragment() {
}
viewModel.showWarning.observe { show ->
- binder.bridgeHezToDotWarning.visibility = if (show) View.VISIBLE else View.GONE
+ binder.bridgeWarningAlert.setVisible(show)
}
viewModel.warningBlocked.observe { blocked ->
- if (blocked) {
- binder.bridgeHezToDotWarning.setBackgroundColor(resources.getColor(R.color.error_block_background, null))
- } else {
- binder.bridgeHezToDotWarning.setBackgroundColor(resources.getColor(R.color.warning_block_background, null))
- }
+ binder.bridgeWarningAlert.setStylePreset(
+ if (blocked) AlertView.StylePreset.ERROR else AlertView.StylePreset.WARNING
+ )
}
viewModel.warningText.observe { text ->
if (text.isNotEmpty()) {
- binder.bridgeHezToDotWarning.text = text
- }
- }
- }
-
- private fun updatePairUI(pair: BridgePair) {
- when (pair) {
- BridgePair.DOT_HEZ -> {
- binder.bridgePairDotHez.setBackgroundResource(R.drawable.bg_button_primary)
- binder.bridgePairDotHez.setTextColor(resources.getColor(R.color.text_primary, null))
- binder.bridgePairUsdt.background = null
- binder.bridgePairUsdt.setTextColor(resources.getColor(R.color.text_secondary, null))
- }
- BridgePair.USDT -> {
- binder.bridgePairUsdt.setBackgroundResource(R.drawable.bg_button_primary)
- binder.bridgePairUsdt.setTextColor(resources.getColor(R.color.text_primary, null))
- binder.bridgePairDotHez.background = null
- binder.bridgePairDotHez.setTextColor(resources.getColor(R.color.text_secondary, null))
- }
- }
- }
-
- private fun updateDirectionUI(direction: BridgeDirection) {
- val isLeft = direction == BridgeDirection.DOT_TO_HEZ || direction == BridgeDirection.USDT_TO_WUSDT
-
- if (isLeft) {
- binder.bridgeDirectionLeft.setBackgroundResource(R.drawable.bg_button_primary)
- binder.bridgeDirectionLeft.setTextColor(resources.getColor(R.color.text_primary, null))
- binder.bridgeDirectionRight.background = null
- binder.bridgeDirectionRight.setTextColor(resources.getColor(R.color.text_secondary, null))
- } else {
- binder.bridgeDirectionRight.setBackgroundResource(R.drawable.bg_button_primary)
- binder.bridgeDirectionRight.setTextColor(resources.getColor(R.color.text_primary, null))
- binder.bridgeDirectionLeft.background = null
- binder.bridgeDirectionLeft.setTextColor(resources.getColor(R.color.text_secondary, null))
- }
-
- when (direction) {
- BridgeDirection.DOT_TO_HEZ -> {
- binder.bridgeDirectionLeft.text = "DOT → HEZ"
- binder.bridgeDirectionRight.text = "HEZ → DOT"
- binder.bridgeFromToken.text = "DOT"
- binder.bridgeToToken.text = "HEZ"
- }
- BridgeDirection.HEZ_TO_DOT -> {
- binder.bridgeDirectionLeft.text = "DOT → HEZ"
- binder.bridgeDirectionRight.text = "HEZ → DOT"
- binder.bridgeFromToken.text = "HEZ"
- binder.bridgeToToken.text = "DOT"
- }
- BridgeDirection.USDT_TO_WUSDT -> {
- binder.bridgeDirectionLeft.text = "USDT(Pol) → USDT(Pez)"
- binder.bridgeDirectionRight.text = "USDT(Pez) → USDT(Pol)"
- binder.bridgeFromToken.text = "USDT"
- binder.bridgeToToken.text = "USDT"
- }
- BridgeDirection.WUSDT_TO_USDT -> {
- binder.bridgeDirectionLeft.text = "USDT(Pol) → USDT(Pez)"
- binder.bridgeDirectionRight.text = "USDT(Pez) → USDT(Pol)"
- binder.bridgeFromToken.text = "USDT"
- binder.bridgeToToken.text = "USDT"
+ binder.bridgeWarningAlert.setMessage(text)
}
}
}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgePairListBottomSheet.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgePairListBottomSheet.kt
new file mode 100644
index 00000000..189f3cda
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgePairListBottomSheet.kt
@@ -0,0 +1,68 @@
+package io.novafoundation.nova.feature_assets.presentation.bridge
+
+import android.content.Context
+import android.os.Bundle
+import androidx.recyclerview.widget.DiffUtil
+import coil.ImageLoader
+import io.novafoundation.nova.common.di.FeatureUtils
+import io.novafoundation.nova.common.utils.images.Icon
+import io.novafoundation.nova.common.utils.images.setIconOrMakeGone
+import io.novafoundation.nova.common.utils.inflater
+import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet
+import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.DynamicListSheetAdapter
+import io.novafoundation.nova.common.view.bottomSheet.list.dynamic.HolderCreator
+import io.novafoundation.nova.feature_assets.R
+import io.novafoundation.nova.feature_assets.databinding.ItemBridgePairListBinding
+
+data class BridgePairUi(
+ val pair: BridgePair,
+ val icon: Icon,
+ val title: String
+)
+
+class BridgePairListBottomSheet(
+ context: Context,
+ data: List,
+ onClicked: (BridgePairUi) -> Unit
+) : DynamicListBottomSheet(
+ context,
+ Payload(data),
+ BridgePairDiffCallback,
+ onClicked = { _, item -> onClicked(item) }
+) {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setTitle(R.string.bridge_title)
+ }
+
+ override fun holderCreator(): HolderCreator = {
+ BridgePairListHolder(ItemBridgePairListBinding.inflate(it.inflater(), it, false))
+ }
+}
+
+class BridgePairListHolder(
+ private val binder: ItemBridgePairListBinding
+) : DynamicListSheetAdapter.Holder(binder.root) {
+
+ private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) {
+ FeatureUtils.getCommonApi(binder.root.context).imageLoader()
+ }
+
+ override fun bind(item: BridgePairUi, isSelected: Boolean, handler: DynamicListSheetAdapter.Handler) {
+ binder.itemBridgePairIcon.setIconOrMakeGone(item.icon, imageLoader)
+ binder.itemBridgePairTitle.text = item.title
+ binder.root.setOnClickListener { handler.itemClicked(item) }
+ }
+}
+
+private object BridgePairDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: BridgePairUi, newItem: BridgePairUi): Boolean {
+ return oldItem.pair == newItem.pair
+ }
+
+ override fun areContentsTheSame(oldItem: BridgePairUi, newItem: BridgePairUi): Boolean {
+ return true
+ }
+}
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
index 8421c1e0..1aff0826 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
@@ -4,7 +4,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ResourceManager
+import io.novafoundation.nova.common.utils.images.Icon
import io.novafoundation.nova.common.view.ButtonState
+import io.novafoundation.nova.feature_account_api.presenatation.chain.asIconOrFallback
import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload
@@ -12,6 +14,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
+import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -76,6 +79,15 @@ class BridgeViewModel(
private val _warningText = MutableLiveData()
val warningText: LiveData = _warningText
+ private val _fromCard = MutableLiveData()
+ val fromCard: LiveData = _fromCard
+
+ private val _toCard = MutableLiveData()
+ val toCard: LiveData = _toCard
+
+ private val _pairOptions = MutableLiveData>(emptyList())
+ val pairOptions: LiveData> = _pairOptions
+
private var currentAmount: Double = 0.0
private var dotToHezRate: Double = FALLBACK_RATE
private var isHezToDotActive: Boolean = false
@@ -84,6 +96,8 @@ class BridgeViewModel(
init {
fetchExchangeRate()
fetchBridgeStatus()
+ updateCards()
+ loadPairOptions()
}
fun setPair(newPair: BridgePair) {
@@ -97,6 +111,7 @@ class BridgeViewModel(
updateUI()
calculateOutput()
updateWarningState()
+ updateCards()
}
}
@@ -111,6 +126,7 @@ class BridgeViewModel(
updateUI()
calculateOutput()
updateWarningState()
+ updateCards()
}
}
@@ -125,6 +141,7 @@ class BridgeViewModel(
updateUI()
calculateOutput()
updateWarningState()
+ updateCards()
}
}
@@ -322,4 +339,65 @@ class BridgeViewModel(
fun refreshBridgeStatus() {
fetchBridgeStatus()
}
+
+ private fun updateCards() {
+ val dir = _direction.value ?: return
+
+ // Same chainId/assetId mapping already used by swapClicked() to resolve the origin side -
+ // mirrored here (plus its destination counterpart) purely to display logos/names, no new business rule.
+ val originChainId = when (dir) {
+ BridgeDirection.DOT_TO_HEZ, BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
+ BridgeDirection.HEZ_TO_DOT, BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
+ }
+ val destChainId = when (dir) {
+ BridgeDirection.DOT_TO_HEZ, BridgeDirection.USDT_TO_WUSDT -> PEZKUWI_ASSET_HUB_ID
+ BridgeDirection.HEZ_TO_DOT, BridgeDirection.WUSDT_TO_USDT -> POLKADOT_ASSET_HUB_ID
+ }
+ val originAssetId = when (dir) {
+ BridgeDirection.DOT_TO_HEZ, BridgeDirection.HEZ_TO_DOT -> UTILITY_ASSET_ID
+ BridgeDirection.USDT_TO_WUSDT -> POLKADOT_USDT_ASSET_ID
+ BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_USDT_ASSET_ID
+ }
+ val destAssetId = when (dir) {
+ BridgeDirection.DOT_TO_HEZ, BridgeDirection.HEZ_TO_DOT -> UTILITY_ASSET_ID
+ BridgeDirection.USDT_TO_WUSDT -> PEZKUWI_USDT_ASSET_ID
+ BridgeDirection.WUSDT_TO_USDT -> POLKADOT_USDT_ASSET_ID
+ }
+
+ launch {
+ _fromCard.value = cardUiFor(originChainId, originAssetId)
+ _toCard.value = cardUiFor(destChainId, destAssetId)
+ }
+ }
+
+ private fun loadPairOptions() {
+ launch {
+ val dotHezIcon = cardUiFor(POLKADOT_ASSET_HUB_ID, UTILITY_ASSET_ID).assetIcon
+ val usdtIcon = cardUiFor(POLKADOT_ASSET_HUB_ID, POLKADOT_USDT_ASSET_ID).assetIcon
+
+ _pairOptions.value = listOf(
+ BridgePairUi(BridgePair.DOT_HEZ, dotHezIcon, resourceManager.getString(R.string.bridge_pair_dot_hez)),
+ BridgePairUi(BridgePair.USDT, usdtIcon, resourceManager.getString(R.string.bridge_pair_usdt))
+ )
+ }
+ }
+
+ private suspend fun cardUiFor(chainId: String, assetId: Int): BridgeAssetCardUi {
+ val chain = chainRegistry.getChain(chainId)
+ val asset = chain.assetsById.getValue(assetId)
+
+ return BridgeAssetCardUi(
+ assetIcon = asset.icon.asIconOrFallback(),
+ chainIconUrl = chain.icon,
+ symbol = asset.symbol.value,
+ chainName = chain.name
+ )
+ }
}
+
+data class BridgeAssetCardUi(
+ val assetIcon: Icon,
+ val chainIconUrl: String?,
+ val symbol: String,
+ val chainName: String
+)
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt
new file mode 100644
index 00000000..b22ec0a1
--- /dev/null
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/view/BridgeAssetInputView.kt
@@ -0,0 +1,63 @@
+package io.novafoundation.nova.feature_assets.presentation.bridge.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.EditText
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import coil.ImageLoader
+import io.novafoundation.nova.common.di.FeatureUtils
+import io.novafoundation.nova.common.utils.WithContextExtensions
+import io.novafoundation.nova.common.utils.images.asUrlIcon
+import io.novafoundation.nova.common.utils.images.setIconOrMakeGone
+import io.novafoundation.nova.common.utils.inflater
+import io.novafoundation.nova.common.view.shape.getInputBackground
+import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
+import io.novafoundation.nova.feature_assets.databinding.ViewBridgeAssetInputBinding
+import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeAssetCardUi
+
+class BridgeAssetInputView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr),
+ WithContextExtensions by WithContextExtensions(context) {
+
+ private val binder = ViewBridgeAssetInputBinding.inflate(inflater(), this)
+
+ val amountInput: EditText
+ get() = binder.bridgeAssetInputField
+
+ private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) {
+ FeatureUtils.getCommonApi(context).imageLoader()
+ }
+
+ init {
+ binder.bridgeAssetInputContainer.background = context.getInputBackground()
+ }
+
+ fun setCardClickListener(listener: OnClickListener) {
+ binder.bridgeAssetInputContainer.setOnClickListener(listener)
+ binder.bridgeAssetInputChevron.isVisible = true
+ }
+
+ fun setEditable(editable: Boolean) {
+ amountInput.isFocusable = editable
+ amountInput.isFocusableInTouchMode = editable
+ amountInput.isClickable = editable
+ amountInput.isCursorVisible = editable
+ }
+
+ fun setAmountText(text: CharSequence) {
+ if (amountInput.text?.toString() != text.toString()) {
+ amountInput.setText(text)
+ }
+ }
+
+ fun setModel(model: BridgeAssetCardUi) {
+ binder.bridgeAssetInputImage.setTokenIcon(model.assetIcon, imageLoader)
+ binder.bridgeAssetInputToken.text = model.symbol
+ binder.bridgeAssetInputSubtitle.text = model.chainName
+ binder.bridgeAssetInputSubtitleImage.setIconOrMakeGone(model.chainIconUrl?.asUrlIcon(), imageLoader)
+ }
+}
diff --git a/feature-assets/src/main/res/layout/fragment_bridge.xml b/feature-assets/src/main/res/layout/fragment_bridge.xml
index 4bf845b2..21a95857 100644
--- a/feature-assets/src/main/res/layout/fragment_bridge.xml
+++ b/feature-assets/src/main/res/layout/fragment_bridge.xml
@@ -25,212 +25,51 @@
android:orientation="vertical"
android:padding="16dp">
-
-
+
+
+ android:layout_marginTop="8dp" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_flip_swap" />
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="8dp" />
@@ -317,18 +156,14 @@
-
-
+
+ android:visibility="gone"
+ app:alertMode="warning" />
diff --git a/feature-assets/src/main/res/layout/item_bridge_pair_list.xml b/feature-assets/src/main/res/layout/item_bridge_pair_list.xml
new file mode 100644
index 00000000..8c5f6b3b
--- /dev/null
+++ b/feature-assets/src/main/res/layout/item_bridge_pair_list.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/feature-assets/src/main/res/layout/view_bridge_asset_input.xml b/feature-assets/src/main/res/layout/view_bridge_asset_input.xml
new file mode 100644
index 00000000..6fd17556
--- /dev/null
+++ b/feature-assets/src/main/res/layout/view_bridge_asset_input.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+