feat: add Start Tracking button for StakingScore on dashboard

Query StakingScore::StakingStartBlock from People Chain to check
tracking status. Show "Start Tracking" button for approved citizens
who haven't opted in yet. Submits start_score_tracking() extrinsic
with loading state and error handling to prevent duplicate calls.
This commit is contained in:
2026-03-05 02:33:31 +03:00
parent 6939dfd4c3
commit 91f2b5d254
10 changed files with 114 additions and 9 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ dependencies {
implementation insetterDep
implementation shimmerDep
implementation flexBoxDep
api flexBoxDep
implementation chartsDep
@@ -7,5 +7,6 @@ data class PezkuwiDashboardData(
val roles: List<String>,
val trustScore: BigInteger,
val welatiCount: Int,
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED,
val isTrackingScore: Boolean = false
)
@@ -36,12 +36,14 @@ class PezkuwiDashboardRepository(
val trustScore = queryTrustScore(chainId, accountId)
val welatiCount = fetchWelatiCount()
val kycStatus = runCatching { queryKycStatus(chainId, accountId) }.getOrDefault(CitizenshipStatus.NOT_STARTED)
val isTrackingScore = queryIsTrackingScore(chainId, accountId)
return PezkuwiDashboardData(
roles = roles.ifEmpty { listOf("Non-Citizen") },
trustScore = trustScore,
welatiCount = welatiCount,
citizenshipStatus = kycStatus
citizenshipStatus = kycStatus,
isTrackingScore = isTrackingScore
)
}
@@ -73,6 +75,15 @@ class PezkuwiDashboardRepository(
}
}.getOrDefault(BigInteger.ZERO)
suspend fun queryIsTrackingScore(chainId: String, accountId: AccountId): Boolean = runCatching {
remoteStorageDataSource.query(chainId) {
val module = runtime.metadata.moduleOrNull("StakingScore") ?: return@query false
module.storage("StakingStartBlock").query(accountId, binding = { decoded ->
decoded != null
})
}
}.getOrDefault(false)
suspend fun queryFreeBalance(chainId: String, accountId: AccountId): BigInteger = runCatching {
remoteStorageDataSource.query(chainId) {
val systemModule = runtime.metadata.moduleOrNull("System") ?: return@query BigInteger.ZERO
@@ -33,6 +33,7 @@ import io.novafoundation.nova.feature_assets.presentation.balance.list.view.Asse
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsAdapter
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsHolder
import android.content.Intent
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.PezkuwiDashboardHolder
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet
@@ -189,6 +190,14 @@ class BalanceListFragment :
}
startActivity(Intent.createChooser(intent, null))
}
viewModel.showTrackingSuccessEvent.observeEvent {
Toast.makeText(requireContext(), R.string.pezkuwi_dashboard_tracking_success, Toast.LENGTH_SHORT).show()
}
viewModel.trackingLoading.observe(viewLifecycleOwner) { loading ->
pezkuwiDashboardAdapter.setTrackingLoading(loading)
}
}
override fun assetClicked(asset: Chain.Asset) {
@@ -278,6 +287,10 @@ class BalanceListFragment :
viewModel.shareReferralClicked()
}
override fun onStartTrackingClicked() {
viewModel.startTrackingClicked()
}
private fun setupRecyclerViewSpacing() {
binder.balanceListAssets.addSpaceItemDecoration {
add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8))
@@ -65,7 +65,12 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.
import io.novafoundation.nova.feature_wallet_api.presentation.model.FractionPartStyling
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
import io.novafoundation.nova.feature_wallet_connect_api.presentation.mapNumberOfActiveSessionsToUi
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
import java.text.NumberFormat
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.combine
@@ -104,7 +109,9 @@ class BalanceListViewModel(
private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
private val maskingModeUseCase: MaskingModeUseCase,
private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
private val pezkuwiDashboardInteractor: PezkuwiDashboardInteractor
private val pezkuwiDashboardInteractor: PezkuwiDashboardInteractor,
private val extrinsicService: ExtrinsicService,
private val chainRegistry: ChainRegistry
) : BaseViewModel(), Browserable.Presentation by Browserable() {
private val maskableAmountFormatterFlow = maskableValueFormatterProvider.provideFormatter()
@@ -122,6 +129,12 @@ class BalanceListViewModel(
private val _shareReferralEvent = MutableLiveData<Event<String>>()
val shareReferralEvent: LiveData<Event<String>> = _shareReferralEvent
private val _showTrackingSuccessEvent = MutableLiveData<Event<Unit>>()
val showTrackingSuccessEvent: LiveData<Event<Unit>> = _showTrackingSuccessEvent
private val _trackingLoading = MutableLiveData(false)
val trackingLoading: LiveData<Boolean> = _trackingLoading
val bannersMixin = promotionBannersMixinFactory.create(bannerSourceFactory.assetsSource(), viewModelScope)
private val selectedCurrency = currencyInteractor.observeSelectCurrency()
@@ -239,7 +252,8 @@ class BalanceListViewModel(
roles = data.roles,
trustScore = data.trustScore.toString(),
welatiCount = NumberFormat.getIntegerInstance().format(data.welatiCount),
citizenshipStatus = data.citizenshipStatus
citizenshipStatus = data.citizenshipStatus,
isTrackingScore = data.isTrackingScore
)
}
.getOrNull()
@@ -425,6 +439,31 @@ class BalanceListViewModel(
_shareReferralEvent.postValue(Event(shareText))
}
fun startTrackingClicked() {
if (_trackingLoading.value == true) return
_trackingLoading.value = true
launchUnit {
try {
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
val result = extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet) {
call(
moduleName = "StakingScore",
callName = "start_score_tracking",
arguments = emptyMap()
)
}
result.getOrThrow()
_showTrackingSuccessEvent.postValue(Event(Unit))
fullSync()
} catch (e: Exception) {
showError(e.message ?: "Score tracking failed")
} finally {
_trackingLoading.postValue(false)
}
}
}
fun novaCardClicked() = launchUnit {
novaCardRestrictionCheckMixin.checkRestrictionAndDo {
router.openNovaCard()
@@ -42,6 +42,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
@Module(includes = [ViewModelModule::class])
@@ -127,6 +128,8 @@ class BalanceListModule {
fiatFormatter: FiatFormatter,
giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
pezkuwiDashboardInteractor: PezkuwiDashboardInteractor,
extrinsicService: ExtrinsicService,
chainRegistry: ChainRegistry,
): ViewModel {
return BalanceListViewModel(
promotionBannersMixinFactory = promotionBannersMixinFactory,
@@ -149,7 +152,9 @@ class BalanceListModule {
maskingModeUseCase = maskingModeUseCase,
fiatFormatter = fiatFormatter,
giftsRestrictionCheckMixin = giftsRestrictionCheckMixin,
pezkuwiDashboardInteractor = pezkuwiDashboardInteractor
pezkuwiDashboardInteractor = pezkuwiDashboardInteractor,
extrinsicService = extrinsicService,
chainRegistry = chainRegistry
)
}
@@ -6,5 +6,6 @@ data class PezkuwiDashboardModel(
val roles: List<String>,
val trustScore: String,
val welatiCount: String,
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED,
val isTrackingScore: Boolean = false
)
@@ -23,22 +23,29 @@ class PezkuwiDashboardAdapter(
fun onBasvuruClicked()
fun onSignClicked()
fun onShareReferralClicked()
fun onStartTrackingClicked()
}
private var model: PezkuwiDashboardModel? = null
private var trackingLoading: Boolean = false
fun setModel(model: PezkuwiDashboardModel) {
this.model = model
notifyChangedIfShown()
}
fun setTrackingLoading(loading: Boolean) {
this.trackingLoading = loading
notifyChangedIfShown()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PezkuwiDashboardHolder {
val binding = ItemPezkuwiDashboardBinding.inflate(parent.inflater(), parent, false)
return PezkuwiDashboardHolder(binding, handler)
}
override fun onBindViewHolder(holder: PezkuwiDashboardHolder, position: Int) {
model?.let { holder.bind(it) }
model?.let { holder.bind(it, trackingLoading) }
}
override fun getItemViewType(position: Int): Int {
@@ -59,13 +66,23 @@ class PezkuwiDashboardHolder(
binder.pezkuwiDashboardBasvuruButton.setOnClickListener { handler.onBasvuruClicked() }
binder.pezkuwiDashboardSignButton.setOnClickListener { handler.onSignClicked() }
binder.pezkuwiDashboardShareButton.setOnClickListener { handler.onShareReferralClicked() }
binder.pezkuwiDashboardStartTrackingButton.setOnClickListener { handler.onStartTrackingClicked() }
}
fun bind(model: PezkuwiDashboardModel) {
fun bind(model: PezkuwiDashboardModel, trackingLoading: Boolean = false) {
bindRoles(model.roles)
binder.pezkuwiDashboardTrustValue.text = model.trustScore
binder.pezkuwiDashboardWelatiCount.text = model.welatiCount
bindButtons(model.citizenshipStatus)
val showTracking = !model.isTrackingScore && model.citizenshipStatus == CitizenshipStatus.APPROVED
binder.pezkuwiDashboardStartTrackingButton.visibility = if (showTracking) View.VISIBLE else View.GONE
if (showTracking) {
binder.pezkuwiDashboardStartTrackingButton.isEnabled = !trackingLoading
binder.pezkuwiDashboardStartTrackingButton.text = if (trackingLoading) "..." else
binder.root.context.getString(R.string.pezkuwi_dashboard_start_tracking)
}
}
private fun bindButtons(status: CitizenshipStatus) {
@@ -69,6 +69,22 @@
android:textColor="#FFD54F"
android:textSize="16sp"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pezkuwiDashboardStartTrackingButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:minWidth="0dp"
android:paddingHorizontal="10dp"
android:text="@string/pezkuwi_dashboard_start_tracking"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="11sp"
android:visibility="gone"
app:backgroundTint="#FF9800"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>