From 91f2b5d254056d5294c0f08a82cd5953b5626a48 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Thu, 5 Mar 2026 02:33:31 +0300 Subject: [PATCH] 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. --- common/src/main/res/values/strings.xml | 2 + feature-assets/build.gradle | 2 +- .../data/model/PezkuwiDashboardData.kt | 3 +- .../repository/PezkuwiDashboardRepository.kt | 13 +++++- .../balance/list/BalanceListFragment.kt | 13 ++++++ .../balance/list/BalanceListViewModel.kt | 43 ++++++++++++++++++- .../balance/list/di/BalanceListModule.kt | 7 ++- .../list/model/PezkuwiDashboardModel.kt | 3 +- .../list/view/PezkuwiDashboardAdapter.kt | 21 ++++++++- .../res/layout/item_pezkuwi_dashboard.xml | 16 +++++++ 10 files changed, 114 insertions(+), 9 deletions(-) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 6d96304..7e68305 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -2766,6 +2766,8 @@ Trust Score Apply & Actions Use our Telegram MiniApp for Digital Kurdistan citizenship services.\n\nTo earn PEZ rewards, you must hold a Welatî ticket and stake at least 10 HEZ.\n\nNon-citizens can only benefit from HEZ rewards. + Start Tracking + Score tracking started! Hejmara Kurd Le Cihane diff --git a/feature-assets/build.gradle b/feature-assets/build.gradle index 7ad29d2..5d2c584 100644 --- a/feature-assets/build.gradle +++ b/feature-assets/build.gradle @@ -95,7 +95,7 @@ dependencies { implementation insetterDep implementation shimmerDep - implementation flexBoxDep + api flexBoxDep implementation chartsDep diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt index 904f52f..c6e4037 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/model/PezkuwiDashboardData.kt @@ -7,5 +7,6 @@ data class PezkuwiDashboardData( val roles: List, val trustScore: BigInteger, val welatiCount: Int, - val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED + val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED, + val isTrackingScore: Boolean = false ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt index 4c7f492..ea5d897 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/data/repository/PezkuwiDashboardRepository.kt @@ -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 diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt index a60e904..e412963 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListFragment.kt @@ -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)) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt index 9a068ed..43d54f2 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt @@ -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>() val shareReferralEvent: LiveData> = _shareReferralEvent + private val _showTrackingSuccessEvent = MutableLiveData>() + val showTrackingSuccessEvent: LiveData> = _showTrackingSuccessEvent + + private val _trackingLoading = MutableLiveData(false) + val trackingLoading: LiveData = _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() diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt index 5389db0..eb4ddf0 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/di/BalanceListModule.kt @@ -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 ) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt index 6e133c2..11a5940 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/model/PezkuwiDashboardModel.kt @@ -6,5 +6,6 @@ data class PezkuwiDashboardModel( val roles: List, val trustScore: String, val welatiCount: String, - val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED + val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED, + val isTrackingScore: Boolean = false ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt index 8508628..e623c5a 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/PezkuwiDashboardAdapter.kt @@ -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) { diff --git a/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml b/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml index 259a33b..f915859 100644 --- a/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml +++ b/feature-assets/src/main/res/layout/item_pezkuwi_dashboard.xml @@ -69,6 +69,22 @@ android:textColor="#FFD54F" android:textSize="16sp" android:textStyle="bold" /> + +