Add a Pending Signatures card to the wallet home screen

Surfaces multisig operations awaiting the current signatory's own
approval directly on the main balance screen, instead of requiring
navigation to the separate pending-operations list. Each row shows the
formatted amount, destination, chain, and approval progress, with a
red Sign button that opens the existing operation-details/approve flow
(fee estimation, signatory balance check, and validation are all
already handled there - not reimplemented here).

Reuses the already-tested MultisigCallFormatter from
feature-multisig:operations (added as a new feature-assets dependency,
exposed via the standard FeatureApi/FeatureContainer pattern - see
AssetsFeatureHolder/AssetsFeatureComponent/AssetsFeatureDependencies)
rather than re-deriving call-preview formatting independently. The
card follows the same SingleItemAdapter + ConcatAdapter pattern as the
existing Pezkuwi Dashboard card on the same screen.

Once a pending operation is actually signed, it naturally drops off
this list on the next sync (no separate client-side "signed" state is
tracked in parallel with the real on-chain approval status).

String only added to the base (English) strings.xml for now - not yet
translated into the other ~14 locales this app otherwise maintains.
This commit is contained in:
2026-07-17 13:03:12 -07:00
parent e1a010b779
commit 707d1baba4
14 changed files with 286 additions and 2 deletions
@@ -108,6 +108,8 @@ import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedBottomSheet import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedBottomSheet
import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedPayload import io.novafoundation.nova.feature_multisig_operations.presentation.created.MultisigCreatedPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsFragment
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
import io.novafoundation.nova.feature_onboarding_impl.OnboardingRouter import io.novafoundation.nova.feature_onboarding_impl.OnboardingRouter
import io.novafoundation.nova.feature_onboarding_impl.presentation.welcome.WelcomeFragment import io.novafoundation.nova.feature_onboarding_impl.presentation.welcome.WelcomeFragment
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
@@ -983,6 +985,12 @@ class Navigator(
.navigateInFirstAttachedContext() .navigateInFirstAttachedContext()
} }
override fun openMultisigOperationDetails(payload: MultisigOperationDetailsPayload) {
navigationBuilder().action(R.id.action_multisigOperationDetailsFragment)
.setArgs(MultisigOperationDetailsFragment.createPayload(payload))
.navigateInFirstAttachedContext()
}
override fun openMainWithFinishMultisigTransaction(accountWasSwitched: Boolean) { override fun openMainWithFinishMultisigTransaction(accountWasSwitched: Boolean) {
val payload = MultisigCreatedBottomSheet.createPayload(MultisigCreatedPayload(accountWasSwitched)) val payload = MultisigCreatedBottomSheet.createPayload(MultisigCreatedPayload(accountWasSwitched))
openSplitScreenWithInstantAction(R.id.action_open_multisigCreatedDialog, nestedActionExtras = payload) openSplitScreenWithInstantAction(R.id.action_open_multisigCreatedDialog, nestedActionExtras = payload)
+3
View File
@@ -2778,6 +2778,9 @@
<string name="pezkuwi_dashboard_tracking_success">Score tracking started!</string> <string name="pezkuwi_dashboard_tracking_success">Score tracking started!</string>
<string name="pezkuwi_dashboard_kurds_title">Hejmara Kurd Le Cihane</string> <string name="pezkuwi_dashboard_kurds_title">Hejmara Kurd Le Cihane</string>
<string name="pending_signatures_title">Pending Signatures</string>
<string name="pending_signatures_sign">Sign</string>
<!-- Citizenship Application --> <!-- Citizenship Application -->
<string name="citizenship_title">Citizenship Application</string> <string name="citizenship_title">Citizenship Application</string>
<string name="citizenship_name">Full Name</string> <string name="citizenship_name">Full Name</string>
+1
View File
@@ -48,6 +48,7 @@ dependencies {
implementation project(':feature-deep-linking') implementation project(':feature-deep-linking')
implementation project(':feature-ahm-api') implementation project(':feature-ahm-api')
implementation project(':feature-gift-api') implementation project(':feature-gift-api')
implementation project(':feature-multisig:operations')
implementation kotlinDep implementation kotlinDep
@@ -53,6 +53,7 @@ import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
import io.novafoundation.nova.feature_nft_api.NftFeatureApi import io.novafoundation.nova.feature_nft_api.NftFeatureApi
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
@@ -175,7 +176,8 @@ interface AssetsFeatureComponent : AssetsFeatureApi {
BannersFeatureApi::class, BannersFeatureApi::class,
DeepLinkingFeatureApi::class, DeepLinkingFeatureApi::class,
ChainMigrationFeatureApi::class, ChainMigrationFeatureApi::class,
GiftFeatureApi::class GiftFeatureApi::class,
MultisigOperationsFeatureApi::class
] ]
) )
interface AssetsFeatureDependenciesComponent : AssetsFeatureDependencies interface AssetsFeatureDependenciesComponent : AssetsFeatureDependencies
@@ -99,6 +99,7 @@ import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeC
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
@@ -240,6 +241,8 @@ interface AssetsFeatureDependencies {
val multisigPendingOperationsService: MultisigPendingOperationsService val multisigPendingOperationsService: MultisigPendingOperationsService
val multisigCallFormatter: MultisigCallFormatter
val automaticInteractionGate: AutomaticInteractionGate val automaticInteractionGate: AutomaticInteractionGate
val linkBuilderFactory: LinkBuilderFactory val linkBuilderFactory: LinkBuilderFactory
@@ -15,6 +15,7 @@ import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
import io.novafoundation.nova.feature_nft_api.NftFeatureApi import io.novafoundation.nova.feature_nft_api.NftFeatureApi
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
@@ -51,6 +52,7 @@ class AssetsFeatureHolder @Inject constructor(
.deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java)) .deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java))
.chainMigrationFeatureApi(getFeature(ChainMigrationFeatureApi::class.java)) .chainMigrationFeatureApi(getFeature(ChainMigrationFeatureApi::class.java))
.giftFeatureApi(getFeature(GiftFeatureApi::class.java)) .giftFeatureApi(getFeature(GiftFeatureApi::class.java))
.multisigOperationsFeatureApi(getFeature(MultisigOperationsFeatureApi::class.java))
.build() .build()
return DaggerAssetsFeatureComponent.factory() return DaggerAssetsFeatureComponent.factory()
@@ -11,6 +11,7 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.A
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensPayload import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensPayload
import io.novafoundation.nova.feature_assets.presentation.trade.webInterface.TradeWebPayload import io.novafoundation.nova.feature_assets.presentation.trade.webInterface.TradeWebPayload
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
@@ -120,6 +121,8 @@ interface AssetsRouter {
fun openPendingMultisigOperations() fun openPendingMultisigOperations()
fun openMultisigOperationDetails(payload: MultisigOperationDetailsPayload)
fun openAssetDetailsFromDeepLink(payload: AssetPayload) fun openAssetDetailsFromDeepLink(payload: AssetPayload)
fun openGifts() fun openGifts()
@@ -36,6 +36,9 @@ import android.content.Intent
import android.widget.Toast import android.widget.Toast
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardAdapter import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardAdapter
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardHolder import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PezkuwiDashboardHolder
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingSignaturesAdapter
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingSignaturesHolder
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet
import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter
@@ -49,6 +52,7 @@ class BalanceListFragment :
AssetsHeaderAdapter.Handler, AssetsHeaderAdapter.Handler,
ManageAssetsAdapter.Handler, ManageAssetsAdapter.Handler,
PezkuwiDashboardAdapter.Handler, PezkuwiDashboardAdapter.Handler,
PendingSignaturesAdapter.Handler,
CitizenshipBottomSheet.CitizenshipDismissListener { CitizenshipBottomSheet.CitizenshipDismissListener {
override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater) override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater)
@@ -66,6 +70,10 @@ class BalanceListFragment :
PezkuwiDashboardAdapter(this) PezkuwiDashboardAdapter(this)
} }
private val pendingSignaturesAdapter by lazy(LazyThreadSafetyMode.NONE) {
PendingSignaturesAdapter(imageLoader, this)
}
private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) { private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) {
PromotionBannerAdapter(closable = true) PromotionBannerAdapter(closable = true)
} }
@@ -86,7 +94,15 @@ class BalanceListFragment :
} }
private val adapter by lazy(LazyThreadSafetyMode.NONE) { private val adapter by lazy(LazyThreadSafetyMode.NONE) {
ConcatAdapter(headerAdapter, pezkuwiDashboardAdapter, bannerAdapter, manageAssetsAdapter, emptyAssetsPlaceholder, assetsAdapter) ConcatAdapter(
headerAdapter,
pezkuwiDashboardAdapter,
pendingSignaturesAdapter,
bannerAdapter,
manageAssetsAdapter,
emptyAssetsPlaceholder,
assetsAdapter
)
} }
override fun applyInsets(rootView: View) { override fun applyInsets(rootView: View) {
@@ -143,6 +159,11 @@ class BalanceListFragment :
} }
} }
viewModel.pendingSignaturesFlow.observe { models ->
pendingSignaturesAdapter.setModels(models)
pendingSignaturesAdapter.show(models.isNotEmpty())
}
viewModel.bannersMixin.bindWithAdapter(bannerAdapter) { viewModel.bannersMixin.bindWithAdapter(bannerAdapter) {
binder.balanceListAssets.invalidateItemDecorations() binder.balanceListAssets.invalidateItemDecorations()
} }
@@ -301,6 +322,10 @@ class BalanceListFragment :
viewModel.startTrackingClicked() viewModel.startTrackingClicked()
} }
override fun onSignClicked(model: PendingSignatureModel) {
viewModel.pendingSignatureSignClicked(model)
}
private fun setupRecyclerViewSpacing() { private fun setupRecyclerViewSpacing() {
binder.balanceListAssets.addSpaceItemDecoration { binder.balanceListAssets.addSpaceItemDecoration {
add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8)) add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8))
@@ -309,6 +334,11 @@ class BalanceListFragment :
add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4)) add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4))
add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4)) add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4))
add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24)) add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24))
add(SpaceBetween(AssetsHeaderHolder, PendingSignaturesHolder, spaceDp = 8))
add(SpaceBetween(PezkuwiDashboardHolder, PendingSignaturesHolder, spaceDp = 8))
add(SpaceBetween(PendingSignaturesHolder, BannerHolder, spaceDp = 4))
add(SpaceBetween(PendingSignaturesHolder, ManageAssetsHolder, spaceDp = 24))
} }
} }
@@ -20,10 +20,15 @@ import io.novafoundation.nova.common.utils.formatting.formatAsPercentage
import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.launchUnit import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.withSafeLoading import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_account_api.data.multisig.model.MultisigAction
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.userAction
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
import io.novafoundation.nova.feature_assets.R import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.domain.WalletInteractor import io.novafoundation.nova.feature_assets.domain.WalletInteractor
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
@@ -42,6 +47,7 @@ import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
import io.novafoundation.nova.feature_assets.presentation.citizenship.PendingCitizenshipReferrer import io.novafoundation.nova.feature_assets.presentation.citizenship.PendingCitizenshipReferrer
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingOperationsCountModel import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingOperationsCountModel
@@ -52,6 +58,10 @@ import io.novafoundation.nova.feature_banners_api.presentation.source.assetsSour
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_currency_api.domain.model.Currency
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.common.MultisigOperationPayload
import io.novafoundation.nova.feature_multisig_operations.presentation.common.fromOperationId
import io.novafoundation.nova.feature_multisig_operations.presentation.details.general.MultisigOperationDetailsPayload
import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_nft_api.data.model.Nft
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
@@ -108,6 +118,7 @@ class BalanceListViewModel(
private val maskableValueFormatterProvider: MaskableValueFormatterProvider, private val maskableValueFormatterProvider: MaskableValueFormatterProvider,
private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory, private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
private val multisigPendingOperationsService: MultisigPendingOperationsService, private val multisigPendingOperationsService: MultisigPendingOperationsService,
private val multisigCallFormatter: MultisigCallFormatter,
private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin, private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
private val maskingModeUseCase: MaskingModeUseCase, private val maskingModeUseCase: MaskingModeUseCase,
private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin, private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
@@ -248,6 +259,38 @@ class BalanceListViewModel(
.combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount) .combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount)
.shareInBackground() .shareInBackground()
val pendingSignaturesFlow = multisigPendingOperationsService.pendingOperations()
.mapLatest { operations ->
val account = selectedMetaAccount.first()
// Only operations still awaiting THIS signatory's own approval - once signed, the
// item naturally drops out here on next sync (no client-side "turn green" state to
// maintain in parallel with the real, authoritative on-chain approval status).
operations
.filter { it.userAction() is MultisigAction.CanApprove }
.map { it.toPendingSignatureUi(account) }
}
.shareInBackground()
fun pendingSignatureSignClicked(model: PendingSignatureModel) {
val operationPayload = MultisigOperationPayload.fromOperationId(model.id)
router.openMultisigOperationDetails(MultisigOperationDetailsPayload(operationPayload))
}
private suspend fun PendingMultisigOperation.toPendingSignatureUi(selectedAccount: MetaAccount): PendingSignatureModel {
val initialOrigin = selectedAccount.requireAccountIdKeyIn(chain)
val formattedCall = multisigCallFormatter.formatPreview(call, initialOrigin, chain)
return PendingSignatureModel(
id = operationId,
chain = mapChainToUi(chain),
title = formattedCall.title,
subtitle = formattedCall.subtitle,
primaryValue = formattedCall.primaryValue,
progress = resourceManager.getString(R.string.multisig_operations_progress, approvals.size.format(), threshold.format())
)
}
val pezkuwiDashboardFlow = selectedMetaAccount val pezkuwiDashboardFlow = selectedMetaAccount
.combine(dashboardRefreshSignal) { account, _ -> account } .combine(dashboardRefreshSignal) { account, _ -> account }
.mapLatest { metaAccount -> .mapLatest { metaAccount ->
@@ -13,6 +13,7 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository
@@ -122,6 +123,7 @@ class BalanceListModule {
amountFormatter: AmountFormatter, amountFormatter: AmountFormatter,
buySellSelectorMixinFactory: BuySellSelectorMixinFactory, buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
multisigPendingOperationsService: MultisigPendingOperationsService, multisigPendingOperationsService: MultisigPendingOperationsService,
multisigCallFormatter: MultisigCallFormatter,
novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin, novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
maskableValueFormatterProvider: MaskableValueFormatterProvider, maskableValueFormatterProvider: MaskableValueFormatterProvider,
maskingModeUseCase: MaskingModeUseCase, maskingModeUseCase: MaskingModeUseCase,
@@ -148,6 +150,7 @@ class BalanceListModule {
maskableValueFormatterProvider = maskableValueFormatterProvider, maskableValueFormatterProvider = maskableValueFormatterProvider,
buySellSelectorMixinFactory = buySellSelectorMixinFactory, buySellSelectorMixinFactory = buySellSelectorMixinFactory,
multisigPendingOperationsService = multisigPendingOperationsService, multisigPendingOperationsService = multisigPendingOperationsService,
multisigCallFormatter = multisigCallFormatter,
novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin, novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin,
maskingModeUseCase = maskingModeUseCase, maskingModeUseCase = maskingModeUseCase,
fiatFormatter = fiatFormatter, fiatFormatter = fiatFormatter,
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_assets.presentation.balance.list.model
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperationId
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
data class PendingSignatureModel(
val id: PendingMultisigOperationId,
val chain: ChainUi,
val title: String,
val subtitle: String?,
val primaryValue: CharSequence?,
val progress: String,
)
@@ -0,0 +1,74 @@
package io.novafoundation.nova.feature_assets.presentation.balance.list.view
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import io.novafoundation.nova.common.list.SingleItemAdapter
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.utils.recyclerView.WithViewType
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
import io.novafoundation.nova.feature_assets.R
import io.novafoundation.nova.feature_assets.databinding.ItemPendingSignatureRowBinding
import io.novafoundation.nova.feature_assets.databinding.ItemPendingSignaturesCardBinding
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PendingSignatureModel
class PendingSignaturesAdapter(
private val imageLoader: ImageLoader,
private val handler: Handler
) : SingleItemAdapter<PendingSignaturesHolder>(isShownByDefault = false) {
interface Handler {
fun onSignClicked(model: PendingSignatureModel)
}
private var models: List<PendingSignatureModel> = emptyList()
fun setModels(models: List<PendingSignatureModel>) {
this.models = models
notifyChangedIfShown()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PendingSignaturesHolder {
val binding = ItemPendingSignaturesCardBinding.inflate(parent.inflater(), parent, false)
return PendingSignaturesHolder(binding, imageLoader, handler)
}
override fun onBindViewHolder(holder: PendingSignaturesHolder, position: Int) {
holder.bind(models)
}
override fun getItemViewType(position: Int): Int {
return PendingSignaturesHolder.viewType
}
}
class PendingSignaturesHolder(
private val binder: ItemPendingSignaturesCardBinding,
private val imageLoader: ImageLoader,
private val handler: PendingSignaturesAdapter.Handler
) : RecyclerView.ViewHolder(binder.root) {
companion object : WithViewType {
override val viewType: Int = R.layout.item_pending_signatures_card
}
fun bind(models: List<PendingSignatureModel>) {
val container = binder.pendingSignaturesRowsContainer
container.removeAllViews()
models.forEach { model ->
val rowBinding = ItemPendingSignatureRowBinding.inflate(container.inflater(), container, false)
rowBinding.pendingSignatureChainIcon.loadChainIcon(model.chain.icon, imageLoader)
// primaryValue (e.g. "100K USDT") is the amount - the whole point of this card - so it
// takes the prominent title line; the call's own title/action name ("Transfer") isn't
// shown at all here, only the destination (subtitle) is, to keep the row to two lines.
rowBinding.pendingSignatureTitle.text = model.primaryValue ?: model.title
rowBinding.pendingSignatureSubtitle.text = listOfNotNull(model.subtitle, model.chain.name, model.progress)
.joinToString(separator = "")
rowBinding.pendingSignatureSignButton.setOnClickListener { handler.onSignClicked(model) }
container.addView(rowBinding.root)
}
}
}
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/pendingSignatureChainIcon"
android:layout_width="28dp"
android:layout_height="28dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_fallback_network_icon" />
<TextView
android:id="@+id/pendingSignatureTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.Caption1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp"
android:textColor="@color/text_primary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/pendingSignatureSubtitle"
app:layout_constraintEnd_toStartOf="@+id/pendingSignatureSignButton"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/pendingSignatureChainIcon"
app:layout_constraintTop_toTopOf="@+id/pendingSignatureChainIcon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="100K USDT" />
<TextView
android:id="@+id/pendingSignatureSubtitle"
style="@style/TextAppearance.NovaFoundation.Regular.Caption2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/text_secondary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@+id/pendingSignatureChainIcon"
app:layout_constraintEnd_toStartOf="@+id/pendingSignatureSignButton"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/pendingSignatureTitle"
app:layout_constraintTop_toBottomOf="@+id/pendingSignatureTitle"
tools:text="Polkadot Asset Hub • 2/3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pendingSignatureSignButton"
style="@style/Widget.Nova.MaterialButton.Negative"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:minWidth="0dp"
android:paddingHorizontal="16dp"
android:text="@string/pending_signatures_sign"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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_marginHorizontal="16dp"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="20dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_pezkuwi_dashboard"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<TextView
style="@style/TextAppearance.NovaFoundation.SemiBold.Caption1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/pending_signatures_title"
android:textColor="@color/text_secondary" />
<LinearLayout
android:id="@+id/pendingSignaturesRowsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>