Match the execution screen's visual polish to the app's own Swap flow

The structural separation (dedicated screen, no amount input) was already
in place, but the screen itself was much sparser than Swap's own execution
UI - just a label and a timer, no "Do not close the app!" warning and no
summary of what's actually being bridged. Added both, reusing components
already proven on Bridge's own input screen (BridgeAssetInputView in
non-editable mode, same as the main screen's fromCard/toCard) rather than
reaching into feature-swap-impl's Swap-specific components.

Also added the signatories' contact channel (t.me/pezkuwidestek) to the
existing "still processing" message, and made explicit that funds are safe
and on-chain even when auto-pay doesn't complete within the wait window -
this is the message shown when a real transfer lands in manual review, not
just the pre-submit consent gate.
This commit is contained in:
2026-07-15 21:47:32 -07:00
parent 3c06ae00e0
commit 272921dcdf
5 changed files with 147 additions and 24 deletions
+2 -1
View File
@@ -375,10 +375,11 @@
<string name="bridge_sign_error">Signing failed, tap to retry</string>
<string name="bridge_deposit_success_label">Bridge complete — funds delivered</string>
<string name="bridge_deposit_pending_message">Still processing - this can take longer if it needs manual multisig review. Check back in a few minutes.</string>
<string name="bridge_deposit_pending_message">Still processing - this can take longer if it needs manual multisig review. Your funds are safe and recorded on-chain. Check back in a few minutes, or reach the signatories at t.me/pezkuwidestek.</string>
<string name="bridge_execution_submitting_label">Submitting transfer…</string>
<string name="bridge_execution_waiting_destination_label">Waiting for confirmation on %s…</string>
<string name="bridge_execution_do_not_close">Do not close the app!</string>
<string name="wallet_asset_buy_sell">Buy/Sell</string>
@@ -28,6 +28,9 @@ class BridgeExecutionFragment : BaseFragment<BridgeExecutionViewModel, FragmentB
binder.bridgeExecutionToolbar.setHomeButtonListener { viewModel.doneClicked() }
binder.bridgeExecutionDoneButton.setOnClickListener { viewModel.doneClicked() }
binder.bridgeExecutionFromCard.setEditable(false)
binder.bridgeExecutionToCard.setEditable(false)
}
override fun inject() {
@@ -58,6 +61,27 @@ class BridgeExecutionFragment : BaseFragment<BridgeExecutionViewModel, FragmentB
viewModel.doneButtonVisible.observe { visible ->
binder.bridgeExecutionDoneButton.visibility = if (visible) View.VISIBLE else View.GONE
// "Do not close the app!" only makes sense while something is genuinely still in
// flight - doneButtonVisible becomes true exactly once the operation has resolved
// (success, pending review, or failure), so its inverse is the same signal without a
// separate LiveData to keep in sync.
binder.bridgeExecutionDoNotClose.visibility = if (visible) View.GONE else View.VISIBLE
}
viewModel.fromCard.observe { model ->
binder.bridgeExecutionFromCard.setModel(model)
}
viewModel.toCard.observe { model ->
binder.bridgeExecutionToCard.setModel(model)
}
viewModel.fromAmountText.observe { text ->
binder.bridgeExecutionFromCard.setAmountText(text)
}
viewModel.toAmountText.observe { text ->
binder.bridgeExecutionToCard.setAmountText(text)
}
// Single source of truth for the alert banner - only OriginFailed (error) and
@@ -4,23 +4,29 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.ExecutionTimerView
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
import io.novafoundation.nova.feature_assets.domain.send.SendInteractor
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
import io.novafoundation.nova.feature_assets.presentation.bridge.BridgeAssetCardUi
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.buildAssetTransfer
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.time.Duration.Companion.seconds
/**
@@ -35,6 +41,7 @@ class BridgeExecutionViewModel(
private val payload: BridgeExecutionPayload,
private val resourceManager: ResourceManager,
private val chainRegistry: ChainRegistry,
private val assetIconProvider: AssetIconProvider,
private val walletInteractor: WalletInteractor,
private val sendInteractor: SendInteractor,
private val sendUseCase: SendUseCase,
@@ -66,7 +73,22 @@ class BridgeExecutionViewModel(
private val _doneButtonVisible = MutableLiveData(false)
val doneButtonVisible: LiveData<Boolean> = _doneButtonVisible
private val _fromCard = MutableLiveData<BridgeAssetCardUi>()
val fromCard: LiveData<BridgeAssetCardUi> = _fromCard
private val _toCard = MutableLiveData<BridgeAssetCardUi>()
val toCard: LiveData<BridgeAssetCardUi> = _toCard
private val _fromAmountText = MutableLiveData("")
val fromAmountText: LiveData<String> = _fromAmountText
private val _toAmountText = MutableLiveData("")
val toAmountText: LiveData<String> = _toAmountText
init {
launch {
loadCards()
}
launch {
submit()
}
@@ -76,6 +98,30 @@ class BridgeExecutionViewModel(
router.back()
}
private suspend fun loadCards() {
val originChain = chainRegistry.getChain(payload.originChainId)
val destChain = chainRegistry.getChain(payload.destChainId)
_fromCard.postValue(cardUiFor(originChain, payload.originAssetId))
_toCard.postValue(cardUiFor(destChain, payload.destAssetId))
_fromAmountText.postValue(BigDecimal.valueOf(payload.amount).stripTrailingZeros().toPlainString())
val netOutput = payload.amount * (1 - FEE_PERCENT)
_toAmountText.postValue(BigDecimal(netOutput).setScale(6, RoundingMode.DOWN).stripTrailingZeros().toPlainString())
}
private suspend fun cardUiFor(chain: Chain, assetId: Int): BridgeAssetCardUi {
val asset = chain.assetsById.getValue(assetId)
return BridgeAssetCardUi(
assetIcon = assetIconProvider.getAssetIconOrFallback(asset),
chainIconUrl = chain.icon,
symbol = asset.symbol.value,
chainName = chain.displayNameWithAssetStandard()
)
}
private suspend fun submit() {
_state.postValue(BridgeExecutionState.SubmittingOrigin)
_label.postValue(resourceManager.getString(R.string.bridge_execution_submitting_label))
@@ -8,6 +8,7 @@ import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
@@ -28,6 +29,7 @@ class BridgeExecutionModule {
payload: BridgeExecutionPayload,
resourceManager: ResourceManager,
chainRegistry: ChainRegistry,
assetIconProvider: AssetIconProvider,
walletInteractor: WalletInteractor,
sendInteractor: SendInteractor,
sendUseCase: SendUseCase,
@@ -38,6 +40,7 @@ class BridgeExecutionModule {
payload,
resourceManager,
chainRegistry,
assetIconProvider,
walletInteractor,
sendInteractor,
sendUseCase,
@@ -13,39 +13,88 @@
app:layout_constraintTop_toTopOf="parent"
app:titleText="@string/bridge_title" />
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp"
app:layout_constraintBottom_toTopOf="@id/bridgeExecutionDoneButton"
app:layout_constraintTop_toBottomOf="@id/bridgeExecutionToolbar">
<TextView
android:id="@+id/bridgeExecutionLabel"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_secondary"
tools:text="Waiting for confirmation on Pezkuwi Asset Hub…" />
android:orientation="vertical"
android:padding="16dp">
<io.novafoundation.nova.common.view.ExecutionTimerView
android:id="@+id/bridgeExecutionTimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<!-- Only visible while a real on-chain operation is genuinely in flight (submitting
or waiting on the destination) - gone once resolved, same as the Swap execution
screen's equivalent warning. -->
<TextView
android:id="@+id/bridgeExecutionDoNotClose"
style="@style/TextAppearance.NovaFoundation.Bold.Title2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/bridge_execution_do_not_close"
android:textColor="@color/text_primary" />
<io.novafoundation.nova.common.view.AlertView
android:id="@+id/bridgeExecutionPendingReviewAlert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:visibility="gone"
app:alertMode="warning" />
<TextView
android:id="@+id/bridgeExecutionLabel"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="@color/text_secondary"
tools:text="Waiting for confirmation on Pezkuwi Asset Hub…" />
</LinearLayout>
<io.novafoundation.nova.common.view.ExecutionTimerView
android:id="@+id/bridgeExecutionTimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<io.novafoundation.nova.common.view.AlertView
android:id="@+id/bridgeExecutionPendingReviewAlert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
app:alertMode="warning" />
<TextView
android:id="@+id/bridgeExecutionFromLabel"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/bridge_you_send"
android:textColor="@color/text_secondary" />
<io.novafoundation.nova.feature_assets.presentation.bridge.view.BridgeAssetInputView
android:id="@+id/bridgeExecutionFromCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/bridgeExecutionToLabel"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/bridge_you_receive"
android:textColor="@color/text_secondary" />
<io.novafoundation.nova.feature_assets.presentation.bridge.view.BridgeAssetInputView
android:id="@+id/bridgeExecutionToCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>
</ScrollView>
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/bridgeExecutionDoneButton"