mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
feat: in-app citizenship application with referral system
- Add CitizenshipBottomSheet with form (name, father, grandfather, mother, tribe, region) - Submit apply_for_citizenship extrinsic with referrer parameter to People Chain - Query KYC status from IdentityKyc pallet and show appropriate UI state - Referrer approval: query Referral + Applications entries, approve pending referrals - Sign (confirm_citizenship) for applicants after referrer approval - Share referral link for citizens to invite others - Dashboard buttons adapt to citizenship status (hide Apply/Sign when approved) - Balance check (1.1 HEZ) only before new applications, not for sign/approve - i18n strings for en, tr, ku
This commit is contained in:
@@ -2763,4 +2763,32 @@
|
||||
<string name="pezkuwi_dashboard_trust_score">Pûana Pêbaweriyê</string>
|
||||
<string name="pezkuwi_dashboard_basvuru">Serlêdan û Karên</string>
|
||||
<string name="pezkuwi_dashboard_info">Ji bo karên hemwelatîbûna Kurdistana Dîjîtal MiniApp\'a me ya Telegram bikar bînin.\n\nJi bo qezenckirina xelatên PEZ, divê hûn xwediyê bilêta Welatî bin û herî kêm 10 HEZ stake kiribe bin.\n\nKesên ne-hemwelatî tenê dikarin ji xelatên HEZ sûd werbigirin.</string>
|
||||
|
||||
<!-- Citizenship Application -->
|
||||
<string name="citizenship_title">Serlêdana Hemwelatîbûnê</string>
|
||||
<string name="citizenship_name">Nav û Paşnav</string>
|
||||
<string name="citizenship_father_name">Navê Bav</string>
|
||||
<string name="citizenship_grandfather_name">Navê Bapîr</string>
|
||||
<string name="citizenship_mother_name">Navê Dayik</string>
|
||||
<string name="citizenship_tribe">Eşîr</string>
|
||||
<string name="citizenship_region">Herêm</string>
|
||||
<string name="citizenship_apply">Serlêdan bike</string>
|
||||
<string name="citizenship_pending">Serlêdana te li benda pejirandinê ye</string>
|
||||
<string name="citizenship_sign">Îmze bike</string>
|
||||
<string name="citizenship_approved">Tu jixwe hemwelatî yî</string>
|
||||
<string name="citizenship_insufficient_balance">Di People Chain de balance têr nîne. Herî kêm 1.1 HEZ pêwîst e.</string>
|
||||
<string name="citizenship_success">Serlêdan bi serkeftî hat şandin</string>
|
||||
<string name="citizenship_share_referral">Ji bo hemwelatîbûna Kurdistana Dîjîtal hevalê xwe vexwîne!\n\nNavnîşana min a referansê:\n%s</string>
|
||||
<string name="citizenship_share_button">Lînka Referansê Parve Bike</string>
|
||||
<string name="citizenship_referrer_hint">Navnîşana Referansê (ne mecbûrî)</string>
|
||||
<string name="citizenship_sign_description">Referansa te serlêdana te pejirand. Ji bo temamkirina hemwelatîbûnê îmze bike.</string>
|
||||
<string name="citizenship_region_bakur">Bakur</string>
|
||||
<string name="citizenship_region_basur">Başûr</string>
|
||||
<string name="citizenship_region_rojava">Rojava</string>
|
||||
<string name="citizenship_region_rojhelat">Rojhelat</string>
|
||||
<string name="citizenship_region_kurdistan">Kurdistan a Sor</string>
|
||||
<string name="citizenship_region_diaspora">Diaspora</string>
|
||||
<string name="citizenship_approve_address_hint">Navnîşana serlêder</string>
|
||||
<string name="citizenship_approve_button">Referralê bipejirîne</string>
|
||||
<string name="citizenship_approve_success">Referral bi serkeftî hat pejirandin</string>
|
||||
</resources>
|
||||
|
||||
@@ -30,4 +30,32 @@
|
||||
<string name="bridge_hez_to_dot_warning">HEZ → DOT yönü manuel işlenir, 24 saate kadar sürebilir.</string>
|
||||
<string name="bridge_hez_to_dot_blocked">HEZ → DOT yönü şu anda devre dışı.</string>
|
||||
<string name="bridge_wusdt_to_usdt_blocked">USDT(Pez) → USDT(Pol) yönü şu anda devre dışı.</string>
|
||||
|
||||
<!-- Citizenship Application -->
|
||||
<string name="citizenship_title">Vatandaşlık Başvurusu</string>
|
||||
<string name="citizenship_name">Ad Soyad</string>
|
||||
<string name="citizenship_father_name">Baba Adı</string>
|
||||
<string name="citizenship_grandfather_name">Dede Adı</string>
|
||||
<string name="citizenship_mother_name">Anne Adı</string>
|
||||
<string name="citizenship_tribe">Aşiret</string>
|
||||
<string name="citizenship_region">Bölge</string>
|
||||
<string name="citizenship_apply">Başvur</string>
|
||||
<string name="citizenship_pending">Başvurunuz onay bekliyor</string>
|
||||
<string name="citizenship_sign">İmzala</string>
|
||||
<string name="citizenship_approved">Zaten vatandaşsınız</string>
|
||||
<string name="citizenship_insufficient_balance">People Chain\'de yetersiz bakiye. En az 1.1 HEZ gerekli.</string>
|
||||
<string name="citizenship_success">Başvuru başarıyla gönderildi</string>
|
||||
<string name="citizenship_share_referral">Dijital Kurdistan vatandaşlığı için arkadaşını davet et!\n\nReferans adresim:\n%s</string>
|
||||
<string name="citizenship_share_button">Referans Linkini Paylaş</string>
|
||||
<string name="citizenship_referrer_hint">Referans Adresi (opsiyonel)</string>
|
||||
<string name="citizenship_sign_description">Referansınız başvurunuzu onayladı. Vatandaşlığı tamamlamak için imzalayın.</string>
|
||||
<string name="citizenship_region_bakur">Bakur</string>
|
||||
<string name="citizenship_region_basur">Başûr</string>
|
||||
<string name="citizenship_region_rojava">Rojava</string>
|
||||
<string name="citizenship_region_rojhelat">Rojhelat</string>
|
||||
<string name="citizenship_region_kurdistan">Kurdistan a Sor</string>
|
||||
<string name="citizenship_region_diaspora">Diaspora</string>
|
||||
<string name="citizenship_approve_address_hint">Başvuranın adresi</string>
|
||||
<string name="citizenship_approve_button">Referral Onayla</string>
|
||||
<string name="citizenship_approve_success">Referral başarıyla onaylandı</string>
|
||||
</resources>
|
||||
|
||||
@@ -2767,4 +2767,32 @@
|
||||
<string name="pezkuwi_dashboard_basvuru">Apply & Actions</string>
|
||||
<string name="pezkuwi_dashboard_info">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.</string>
|
||||
<string name="pezkuwi_dashboard_kurds_title">Hejmara Kurd Le Cihane</string>
|
||||
|
||||
<!-- Citizenship Application -->
|
||||
<string name="citizenship_title">Citizenship Application</string>
|
||||
<string name="citizenship_name">Full Name</string>
|
||||
<string name="citizenship_father_name">Father\'s Name</string>
|
||||
<string name="citizenship_grandfather_name">Grandfather\'s Name</string>
|
||||
<string name="citizenship_mother_name">Mother\'s Name</string>
|
||||
<string name="citizenship_tribe">Tribe</string>
|
||||
<string name="citizenship_region">Region</string>
|
||||
<string name="citizenship_apply">Apply</string>
|
||||
<string name="citizenship_pending">Your application is pending approval</string>
|
||||
<string name="citizenship_sign">Sign</string>
|
||||
<string name="citizenship_approved">You are already a citizen</string>
|
||||
<string name="citizenship_insufficient_balance">Insufficient balance on People Chain. At least 1.1 HEZ required.</string>
|
||||
<string name="citizenship_success">Application submitted successfully</string>
|
||||
<string name="citizenship_share_referral">Refer a friend for Digital Kurdistan citizenship!\n\nMy referrer address:\n%s</string>
|
||||
<string name="citizenship_share_button">Share Referral Link</string>
|
||||
<string name="citizenship_referrer_hint">Referrer Address (optional)</string>
|
||||
<string name="citizenship_sign_description">Your referrer has approved your application. Sign to complete citizenship.</string>
|
||||
<string name="citizenship_region_bakur">Bakur</string>
|
||||
<string name="citizenship_region_basur">Başûr</string>
|
||||
<string name="citizenship_region_rojava">Rojava</string>
|
||||
<string name="citizenship_region_rojhelat">Rojhelat</string>
|
||||
<string name="citizenship_region_kurdistan">Kurdistan a Sor</string>
|
||||
<string name="citizenship_region_diaspora">Diaspora</string>
|
||||
<string name="citizenship_approve_address_hint">Applicant\'s address</string>
|
||||
<string name="citizenship_approve_button">Approve Referral</string>
|
||||
<string name="citizenship_approve_success">Referral approved successfully</string>
|
||||
</resources>
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
package io.novafoundation.nova.feature_assets.data.model
|
||||
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipStatus
|
||||
import java.math.BigInteger
|
||||
|
||||
data class PezkuwiDashboardData(
|
||||
val roles: List<String>,
|
||||
val trustScore: BigInteger,
|
||||
val welatiCount: Int
|
||||
val welatiCount: Int,
|
||||
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED
|
||||
)
|
||||
|
||||
+109
-1
@@ -1,10 +1,16 @@
|
||||
package io.novafoundation.nova.feature_assets.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStructOrNull
|
||||
import io.novafoundation.nova.feature_assets.data.model.PezkuwiDashboardData
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipStatus
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
|
||||
@@ -29,11 +35,13 @@ class PezkuwiDashboardRepository(
|
||||
val roles = queryRoles(chainId, accountId)
|
||||
val trustScore = queryTrustScore(chainId, accountId)
|
||||
val welatiCount = fetchWelatiCount()
|
||||
val kycStatus = runCatching { queryKycStatus(chainId, accountId) }.getOrDefault(CitizenshipStatus.NOT_STARTED)
|
||||
|
||||
return PezkuwiDashboardData(
|
||||
roles = roles.ifEmpty { listOf("Non-Citizen") },
|
||||
trustScore = trustScore,
|
||||
welatiCount = welatiCount
|
||||
welatiCount = welatiCount,
|
||||
citizenshipStatus = kycStatus
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,4 +72,104 @@ class PezkuwiDashboardRepository(
|
||||
})
|
||||
}
|
||||
}.getOrDefault(BigInteger.ZERO)
|
||||
|
||||
suspend fun queryFreeBalance(chainId: String, accountId: AccountId): BigInteger = runCatching {
|
||||
remoteStorageDataSource.query(chainId) {
|
||||
val systemModule = runtime.metadata.moduleOrNull("System") ?: return@query BigInteger.ZERO
|
||||
systemModule.storage("Account").query(accountId, binding = { decoded ->
|
||||
decoded?.let { bindAccountInfo(it).data.free } ?: BigInteger.ZERO
|
||||
})
|
||||
}
|
||||
}.getOrDefault(BigInteger.ZERO)
|
||||
|
||||
suspend fun getPendingApprovals(chain: Chain, referrerAccountId: AccountId): List<PendingApproval> {
|
||||
return runCatching {
|
||||
remoteStorageDataSource.query(chain.id) {
|
||||
val kycModule = runtime.metadata.moduleOrNull("IdentityKyc") ?: return@query emptyList()
|
||||
val results = mutableListOf<PendingApproval>()
|
||||
val seenAccounts = mutableSetOf<String>()
|
||||
|
||||
// 1) Confirmed referrals from Referral::Referrals (referred → referrer AccountId)
|
||||
val referralModule = runtime.metadata.moduleOrNull("Referral")
|
||||
if (referralModule != null) {
|
||||
val allReferrals: Map<ByteArray, Any?> = referralModule.storage("Referrals").entries(
|
||||
keyExtractor = { it.component1<ByteArray>() },
|
||||
binding = { decoded, _ -> decoded }
|
||||
)
|
||||
allReferrals.forEach { (referredId, referrerValue) ->
|
||||
val referrer = referrerValue as? ByteArray ?: return@forEach
|
||||
if (!referrer.contentEquals(referrerAccountId)) return@forEach
|
||||
|
||||
val addr = chain.addressOf(referredId)
|
||||
seenAccounts.add(addr)
|
||||
|
||||
val statusName = kycModule.storage("KycStatuses").query(referredId, binding = { d ->
|
||||
d?.castToDictEnum()?.name
|
||||
})
|
||||
val status = mapKycStatus(statusName)
|
||||
results.add(PendingApproval(referredId, addr, status))
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Pending applications not yet in Referral pallet
|
||||
val allApplications: Map<ByteArray, Any?> = kycModule.storage("Applications").entries(
|
||||
keyExtractor = { it.component1<ByteArray>() },
|
||||
binding = { decoded, _ -> decoded }
|
||||
)
|
||||
allApplications.forEach { (applicantId, decoded) ->
|
||||
val struct = decoded?.castToStructOrNull() ?: return@forEach
|
||||
val referrer = struct["referrer"] as? ByteArray ?: return@forEach
|
||||
if (!referrer.contentEquals(referrerAccountId)) return@forEach
|
||||
|
||||
val addr = chain.addressOf(applicantId)
|
||||
if (addr in seenAccounts) return@forEach
|
||||
|
||||
val statusName = kycModule.storage("KycStatuses").query(applicantId, binding = { d ->
|
||||
d?.castToDictEnum()?.name
|
||||
})
|
||||
val status = mapKycStatus(statusName)
|
||||
results.add(PendingApproval(applicantId, addr, status))
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
Log.e("PezkuwiDashboard", "getPendingApprovals failed", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapKycStatus(statusName: String?): CitizenshipStatus {
|
||||
return when (statusName) {
|
||||
"PendingReferral" -> CitizenshipStatus.PENDING_REFERRAL
|
||||
"ReferrerApproved" -> CitizenshipStatus.REFERRER_APPROVED
|
||||
"Approved" -> CitizenshipStatus.APPROVED
|
||||
else -> CitizenshipStatus.NOT_STARTED
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun queryKycStatus(chainId: String, accountId: AccountId): CitizenshipStatus {
|
||||
return remoteStorageDataSource.query(chainId) {
|
||||
val kycModule = runtime.metadata.moduleOrNull("IdentityKyc") ?: run {
|
||||
Log.w("PezkuwiDashboard", "IdentityKyc module not found in metadata")
|
||||
return@query CitizenshipStatus.NOT_STARTED
|
||||
}
|
||||
kycModule.storage("KycStatuses").query(accountId, binding = { decoded ->
|
||||
val enumName = decoded?.castToDictEnum()?.name
|
||||
Log.d("PezkuwiDashboard", "KYC status raw enum: '$enumName' (decoded=$decoded)")
|
||||
when (enumName) {
|
||||
"PendingReferral" -> CitizenshipStatus.PENDING_REFERRAL
|
||||
"ReferrerApproved" -> CitizenshipStatus.REFERRER_APPROVED
|
||||
"Approved" -> CitizenshipStatus.APPROVED
|
||||
else -> CitizenshipStatus.NOT_STARTED
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PendingApproval(
|
||||
val applicantAccountId: ByteArray,
|
||||
val applicantAddress: String,
|
||||
val status: CitizenshipStatus
|
||||
)
|
||||
|
||||
+3
@@ -35,6 +35,7 @@ import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.di
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.di.ManageTokensComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.topup.TopUpAddressCommunicator
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.di.BridgeComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.di.CitizenshipComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.asset.di.AssetSellFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.network.di.NetworkSellFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.provider.di.TradeProviderListComponent
|
||||
@@ -138,6 +139,8 @@ interface AssetsFeatureComponent : AssetsFeatureApi {
|
||||
|
||||
fun waitingNovaCardTopUpComponentFactory(): WaitingNovaCardTopUpComponent.Factory
|
||||
|
||||
fun citizenshipComponentFactory(): CitizenshipComponent.Factory
|
||||
|
||||
fun inject(view: GoToNftsView)
|
||||
|
||||
@Component.Factory
|
||||
|
||||
+10
@@ -5,6 +5,7 @@ import io.novafoundation.nova.feature_assets.data.model.PezkuwiDashboardData
|
||||
import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import java.math.BigInteger
|
||||
|
||||
class PezkuwiDashboardInteractor(
|
||||
private val repository: PezkuwiDashboardRepository,
|
||||
@@ -18,4 +19,13 @@ class PezkuwiDashboardInteractor(
|
||||
|
||||
repository.getDashboard(accountId)
|
||||
}
|
||||
|
||||
suspend fun hasSufficientBalance(metaAccount: MetaAccount, minPlanck: BigInteger): Boolean {
|
||||
return runCatching {
|
||||
val peopleChain = chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
val accountId = metaAccount.accountIdIn(peopleChain) ?: return false
|
||||
val balance = repository.queryFreeBalance(ChainGeneses.PEZKUWI_PEOPLE, accountId)
|
||||
balance >= minPlanck
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -32,8 +32,10 @@ import io.novafoundation.nova.feature_assets.presentation.balance.list.view.Asse
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetsHeaderHolder
|
||||
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 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
|
||||
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.bindWithAdapter
|
||||
@@ -175,6 +177,18 @@ class BalanceListFragment :
|
||||
viewModel.pendingOperationsCountModel.observe(headerAdapter::setPendingOperationsCountModel)
|
||||
viewModel.filtersIndicatorIcon.observe(headerAdapter::setFilterIconRes)
|
||||
viewModel.assetViewModeModelFlow.observe { manageAssetsAdapter.setAssetViewModeModel(it) }
|
||||
|
||||
viewModel.openCitizenshipEvent.observeEvent {
|
||||
CitizenshipBottomSheet().show(childFragmentManager, "citizenship")
|
||||
}
|
||||
|
||||
viewModel.shareReferralEvent.observeEvent { shareText ->
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, shareText)
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetClicked(asset: Chain.Asset) {
|
||||
@@ -256,6 +270,14 @@ class BalanceListFragment :
|
||||
viewModel.basvuruClicked()
|
||||
}
|
||||
|
||||
override fun onSignClicked() {
|
||||
viewModel.basvuruClicked()
|
||||
}
|
||||
|
||||
override fun onShareReferralClicked() {
|
||||
viewModel.shareReferralClicked()
|
||||
}
|
||||
|
||||
private fun setupRecyclerViewSpacing() {
|
||||
binder.balanceListAssets.addSpaceItemDecoration {
|
||||
add(SpaceBetween(AssetsHeaderHolder, PezkuwiDashboardHolder, spaceDp = 8))
|
||||
|
||||
+19
-3
@@ -23,6 +23,7 @@ import io.novafoundation.nova.common.utils.withSafeLoading
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
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.defaultSubstrateAddress
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
|
||||
@@ -64,6 +65,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.model.FractionPart
|
||||
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.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigInteger
|
||||
import java.text.NumberFormat
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -114,6 +116,12 @@ class BalanceListViewModel(
|
||||
private val _showBalanceBreakdownEvent = MutableLiveData<Event<TotalBalanceBreakdownModel>>()
|
||||
val showBalanceBreakdownEvent: LiveData<Event<TotalBalanceBreakdownModel>> = _showBalanceBreakdownEvent
|
||||
|
||||
private val _openCitizenshipEvent = MutableLiveData<Event<Unit>>()
|
||||
val openCitizenshipEvent: LiveData<Event<Unit>> = _openCitizenshipEvent
|
||||
|
||||
private val _shareReferralEvent = MutableLiveData<Event<String>>()
|
||||
val shareReferralEvent: LiveData<Event<String>> = _shareReferralEvent
|
||||
|
||||
val bannersMixin = promotionBannersMixinFactory.create(bannerSourceFactory.assetsSource(), viewModelScope)
|
||||
|
||||
private val selectedCurrency = currencyInteractor.observeSelectCurrency()
|
||||
@@ -230,7 +238,8 @@ class BalanceListViewModel(
|
||||
PezkuwiDashboardModel(
|
||||
roles = data.roles,
|
||||
trustScore = data.trustScore.toString(),
|
||||
welatiCount = NumberFormat.getIntegerInstance().format(data.welatiCount)
|
||||
welatiCount = NumberFormat.getIntegerInstance().format(data.welatiCount),
|
||||
citizenshipStatus = data.citizenshipStatus
|
||||
)
|
||||
}
|
||||
.getOrNull()
|
||||
@@ -400,8 +409,15 @@ class BalanceListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun basvuruClicked() {
|
||||
showBrowser("https://t.me/pezkuwichainBot")
|
||||
fun basvuruClicked() = launchUnit {
|
||||
_openCitizenshipEvent.postValue(Event(Unit))
|
||||
}
|
||||
|
||||
fun shareReferralClicked() = launchUnit {
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
val address = metaAccount.defaultSubstrateAddress ?: return@launchUnit
|
||||
val shareText = resourceManager.getString(R.string.citizenship_share_referral, address)
|
||||
_shareReferralEvent.postValue(Event(shareText))
|
||||
}
|
||||
|
||||
fun novaCardClicked() = launchUnit {
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.list.model
|
||||
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipStatus
|
||||
|
||||
data class PezkuwiDashboardModel(
|
||||
val roles: List<String>,
|
||||
val trustScore: String,
|
||||
val welatiCount: String
|
||||
val welatiCount: String,
|
||||
val citizenshipStatus: CitizenshipStatus = CitizenshipStatus.NOT_STARTED
|
||||
)
|
||||
|
||||
+32
@@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_assets.presentation.balance.list.view
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
@@ -12,6 +13,7 @@ import io.novafoundation.nova.common.utils.recyclerView.WithViewType
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemPezkuwiDashboardBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.PezkuwiDashboardModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipStatus
|
||||
|
||||
class PezkuwiDashboardAdapter(
|
||||
private val handler: Handler
|
||||
@@ -19,6 +21,8 @@ class PezkuwiDashboardAdapter(
|
||||
|
||||
interface Handler {
|
||||
fun onBasvuruClicked()
|
||||
fun onSignClicked()
|
||||
fun onShareReferralClicked()
|
||||
}
|
||||
|
||||
private var model: PezkuwiDashboardModel? = null
|
||||
@@ -53,12 +57,40 @@ class PezkuwiDashboardHolder(
|
||||
|
||||
init {
|
||||
binder.pezkuwiDashboardBasvuruButton.setOnClickListener { handler.onBasvuruClicked() }
|
||||
binder.pezkuwiDashboardSignButton.setOnClickListener { handler.onSignClicked() }
|
||||
binder.pezkuwiDashboardShareButton.setOnClickListener { handler.onShareReferralClicked() }
|
||||
}
|
||||
|
||||
fun bind(model: PezkuwiDashboardModel) {
|
||||
bindRoles(model.roles)
|
||||
binder.pezkuwiDashboardTrustValue.text = model.trustScore
|
||||
binder.pezkuwiDashboardWelatiCount.text = model.welatiCount
|
||||
bindButtons(model.citizenshipStatus)
|
||||
}
|
||||
|
||||
private fun bindButtons(status: CitizenshipStatus) {
|
||||
if (status == CitizenshipStatus.APPROVED) {
|
||||
// Citizen: "Onayla" (approve referrals) + Share
|
||||
binder.pezkuwiDashboardBasvuruButton.visibility = View.VISIBLE
|
||||
binder.pezkuwiDashboardBasvuruButton.setText(R.string.citizenship_approve_button)
|
||||
binder.pezkuwiDashboardSignButton.visibility = View.GONE
|
||||
binder.pezkuwiDashboardShareButton.visibility = View.VISIBLE
|
||||
binder.pezkuwiDashboardShareButton.isEnabled = true
|
||||
binder.pezkuwiDashboardShareButton.alpha = 1f
|
||||
} else {
|
||||
// Not yet citizen: show all 3
|
||||
binder.pezkuwiDashboardBasvuruButton.visibility = View.VISIBLE
|
||||
binder.pezkuwiDashboardBasvuruButton.setText(R.string.pezkuwi_dashboard_basvuru)
|
||||
binder.pezkuwiDashboardSignButton.visibility = View.VISIBLE
|
||||
binder.pezkuwiDashboardShareButton.visibility = View.VISIBLE
|
||||
|
||||
val signEnabled = status == CitizenshipStatus.REFERRER_APPROVED
|
||||
binder.pezkuwiDashboardSignButton.isEnabled = signEnabled
|
||||
binder.pezkuwiDashboardSignButton.alpha = if (signEnabled) 1f else 0.4f
|
||||
|
||||
binder.pezkuwiDashboardShareButton.isEnabled = false
|
||||
binder.pezkuwiDashboardShareButton.alpha = 0.4f
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindRoles(roles: List<String>) {
|
||||
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.citizenship
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import io.novafoundation.nova.common.base.BaseBottomSheetFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.data.repository.PendingApproval
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentCitizenshipBottomSheetBinding
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
|
||||
|
||||
class CitizenshipBottomSheet : BaseBottomSheetFragment<CitizenshipViewModel, FragmentCitizenshipBottomSheetBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentCitizenshipBottomSheetBinding.inflate(layoutInflater)
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<AssetsFeatureComponent>(
|
||||
requireContext(),
|
||||
AssetsFeatureApi::class.java
|
||||
)
|
||||
.citizenshipComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
setupRegionSpinner()
|
||||
|
||||
binder.citizenshipActionButton.setOnClickListener {
|
||||
when (viewModel.citizenshipStatus.value) {
|
||||
CitizenshipStatus.NOT_STARTED -> submitForm()
|
||||
CitizenshipStatus.REFERRER_APPROVED -> viewModel.signApplication()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
binder.citizenshipShareButton.setOnClickListener {
|
||||
viewModel.shareReferralLink()
|
||||
}
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: CitizenshipViewModel) {
|
||||
viewModel.citizenshipStatus.observe(viewLifecycleOwner) { status ->
|
||||
updateUiForStatus(status)
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) { loading ->
|
||||
binder.citizenshipProgress.visibility = if (loading) View.VISIBLE else View.GONE
|
||||
binder.citizenshipActionButton.isEnabled = !loading
|
||||
}
|
||||
|
||||
viewModel.dismissEvent.observeEvent {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
viewModel.shareEvent.observeEvent { shareText ->
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, shareText)
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
|
||||
viewModel.pendingApprovals.observe(viewLifecycleOwner) { approvals ->
|
||||
bindInvitationsList(approvals)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRegionSpinner() {
|
||||
val regions = listOf(
|
||||
getString(R.string.citizenship_region_bakur),
|
||||
getString(R.string.citizenship_region_basur),
|
||||
getString(R.string.citizenship_region_rojava),
|
||||
getString(R.string.citizenship_region_rojhelat),
|
||||
getString(R.string.citizenship_region_kurdistan),
|
||||
getString(R.string.citizenship_region_diaspora)
|
||||
)
|
||||
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binder.citizenshipRegionSpinner.adapter = adapter
|
||||
}
|
||||
|
||||
private fun updateUiForStatus(status: CitizenshipStatus) {
|
||||
binder.citizenshipShareButton.visibility = View.GONE
|
||||
binder.citizenshipInvitationsHeader.visibility = View.GONE
|
||||
binder.citizenshipInvitationsScroll.visibility = View.GONE
|
||||
|
||||
when (status) {
|
||||
CitizenshipStatus.LOADING -> {
|
||||
binder.citizenshipFormScroll.visibility = View.GONE
|
||||
binder.citizenshipStatusText.visibility = View.GONE
|
||||
binder.citizenshipActionButton.isEnabled = false
|
||||
binder.citizenshipProgress.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
CitizenshipStatus.NOT_STARTED -> {
|
||||
binder.citizenshipFormScroll.visibility = View.VISIBLE
|
||||
binder.citizenshipStatusText.visibility = View.GONE
|
||||
binder.citizenshipActionButton.isEnabled = true
|
||||
binder.citizenshipActionButton.visibility = View.VISIBLE
|
||||
binder.citizenshipActionButton.text = getString(R.string.citizenship_apply)
|
||||
binder.citizenshipProgress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CitizenshipStatus.PENDING_REFERRAL -> {
|
||||
binder.citizenshipFormScroll.visibility = View.GONE
|
||||
binder.citizenshipStatusText.visibility = View.VISIBLE
|
||||
binder.citizenshipStatusText.text = getString(R.string.citizenship_pending)
|
||||
binder.citizenshipActionButton.isEnabled = false
|
||||
binder.citizenshipActionButton.visibility = View.VISIBLE
|
||||
binder.citizenshipActionButton.text = getString(R.string.citizenship_pending)
|
||||
binder.citizenshipProgress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CitizenshipStatus.REFERRER_APPROVED -> {
|
||||
binder.citizenshipFormScroll.visibility = View.GONE
|
||||
binder.citizenshipStatusText.visibility = View.VISIBLE
|
||||
binder.citizenshipStatusText.text = getString(R.string.citizenship_sign_description)
|
||||
binder.citizenshipActionButton.isEnabled = true
|
||||
binder.citizenshipActionButton.visibility = View.VISIBLE
|
||||
binder.citizenshipActionButton.text = getString(R.string.citizenship_sign)
|
||||
binder.citizenshipProgress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CitizenshipStatus.APPROVED -> {
|
||||
binder.citizenshipFormScroll.visibility = View.GONE
|
||||
binder.citizenshipStatusText.visibility = View.VISIBLE
|
||||
binder.citizenshipStatusText.text = getString(R.string.citizenship_approved)
|
||||
binder.citizenshipActionButton.visibility = View.GONE
|
||||
binder.citizenshipShareButton.visibility = View.VISIBLE
|
||||
binder.citizenshipInvitationsHeader.visibility = View.VISIBLE
|
||||
binder.citizenshipInvitationsScroll.visibility = View.VISIBLE
|
||||
binder.citizenshipProgress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindInvitationsList(approvals: List<PendingApproval>) {
|
||||
val container = binder.citizenshipInvitationsList
|
||||
container.removeAllViews()
|
||||
|
||||
val header = binder.citizenshipInvitationsHeader
|
||||
val pendingCount = approvals.count { it.status == CitizenshipStatus.PENDING_REFERRAL }
|
||||
header.text = "My Invitations (${approvals.size})" +
|
||||
if (pendingCount > 0) " \u2022 $pendingCount pending" else ""
|
||||
|
||||
if (approvals.isEmpty()) {
|
||||
val emptyText = TextView(requireContext()).apply {
|
||||
text = "No invitations yet"
|
||||
setTextColor(Color.parseColor("#78909C"))
|
||||
textSize = 13f
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(0, 8.dp(context), 0, 8.dp(context))
|
||||
}
|
||||
container.addView(emptyText)
|
||||
return
|
||||
}
|
||||
|
||||
approvals.forEach { approval ->
|
||||
container.addView(createInvitationRow(approval))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInvitationRow(approval: PendingApproval): View {
|
||||
val row = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(0, 6.dp(context), 0, 6.dp(context))
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
// Shortened address
|
||||
val addr = approval.applicantAddress
|
||||
val shortAddr = if (addr.length > 16) "${addr.take(8)}...${addr.takeLast(6)}" else addr
|
||||
|
||||
val addressText = TextView(requireContext()).apply {
|
||||
text = shortAddr
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 13f
|
||||
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
row.addView(addressText)
|
||||
|
||||
when (approval.status) {
|
||||
CitizenshipStatus.PENDING_REFERRAL -> {
|
||||
val approveBtn = MaterialButton(requireContext()).apply {
|
||||
text = getString(R.string.citizenship_approve_button)
|
||||
textSize = 11f
|
||||
isAllCaps = false
|
||||
backgroundTintList = ColorStateList.valueOf(Color.parseColor("#4CAF50"))
|
||||
setTextColor(Color.WHITE)
|
||||
cornerRadius = 6.dp(context)
|
||||
setPadding(12.dp(context), 0, 12.dp(context), 0)
|
||||
minimumHeight = 36.dp(context)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
approveBtn.setOnClickListener {
|
||||
viewModel.approveReferral(approval.applicantAccountId)
|
||||
}
|
||||
row.addView(approveBtn)
|
||||
}
|
||||
|
||||
CitizenshipStatus.APPROVED, CitizenshipStatus.REFERRER_APPROVED -> {
|
||||
val confirmedText = TextView(requireContext()).apply {
|
||||
text = "Confirmed"
|
||||
setTextColor(Color.parseColor("#4CAF50"))
|
||||
textSize = 12f
|
||||
}
|
||||
row.addView(confirmedText)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val statusText = TextView(requireContext()).apply {
|
||||
text = approval.status.name
|
||||
setTextColor(Color.parseColor("#78909C"))
|
||||
textSize = 12f
|
||||
}
|
||||
row.addView(statusText)
|
||||
}
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private fun submitForm() {
|
||||
val name = binder.citizenshipNameInput.text?.toString().orEmpty()
|
||||
val fatherName = binder.citizenshipFatherNameInput.text?.toString().orEmpty()
|
||||
val grandfatherName = binder.citizenshipGrandfatherNameInput.text?.toString().orEmpty()
|
||||
val motherName = binder.citizenshipMotherNameInput.text?.toString().orEmpty()
|
||||
val tribe = binder.citizenshipTribeInput.text?.toString().orEmpty()
|
||||
val region = binder.citizenshipRegionSpinner.selectedItem?.toString().orEmpty()
|
||||
val referrer = binder.citizenshipReferrerInput.text?.toString()?.trim()?.ifBlank { null }
|
||||
|
||||
if (name.isBlank()) {
|
||||
binder.citizenshipNameLayout.error = getString(R.string.common_name)
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.submitApplication(name, fatherName, grandfatherName, motherName, tribe, region, referrer)
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.citizenship
|
||||
|
||||
import android.util.Log
|
||||
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.Event
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.data.repository.PendingApproval
|
||||
import io.novafoundation.nova.feature_assets.data.repository.PezkuwiDashboardRepository
|
||||
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 io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
|
||||
import kotlinx.coroutines.launch
|
||||
import org.bouncycastle.jcajce.provider.digest.Keccak
|
||||
import java.math.BigInteger
|
||||
|
||||
enum class CitizenshipStatus {
|
||||
NOT_STARTED,
|
||||
PENDING_REFERRAL,
|
||||
REFERRER_APPROVED,
|
||||
APPROVED,
|
||||
LOADING
|
||||
}
|
||||
|
||||
class CitizenshipViewModel(
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val pezkuwiDashboardRepository: PezkuwiDashboardRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _citizenshipStatus = MutableLiveData(CitizenshipStatus.LOADING)
|
||||
val citizenshipStatus: LiveData<CitizenshipStatus> = _citizenshipStatus
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
|
||||
private val _dismissEvent = MutableLiveData<Event<Unit>>()
|
||||
val dismissEvent: LiveData<Event<Unit>> = _dismissEvent
|
||||
|
||||
private val _shareEvent = MutableLiveData<Event<String>>()
|
||||
val shareEvent: LiveData<Event<String>> = _shareEvent
|
||||
|
||||
private val _pendingApprovals = MutableLiveData<List<PendingApproval>>(emptyList())
|
||||
val pendingApprovals: LiveData<List<PendingApproval>> = _pendingApprovals
|
||||
|
||||
private var peopleChain: Chain? = null
|
||||
private var cachedAccountId: ByteArray? = null
|
||||
|
||||
companion object {
|
||||
private val MIN_BALANCE_PLANCK = BigInteger("1100000000000") // 1.1 HEZ
|
||||
private const val TAG = "CitizenshipVM"
|
||||
}
|
||||
|
||||
init {
|
||||
loadStatus()
|
||||
}
|
||||
|
||||
private fun loadStatus() {
|
||||
launch {
|
||||
try {
|
||||
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
peopleChain = chain
|
||||
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
val accountId = metaAccount.accountIdIn(chain) ?: run {
|
||||
_citizenshipStatus.postValue(CitizenshipStatus.NOT_STARTED)
|
||||
return@launch
|
||||
}
|
||||
cachedAccountId = accountId
|
||||
|
||||
val status = queryKycStatus(accountId)
|
||||
Log.d(TAG, "KYC status loaded: $status")
|
||||
|
||||
// Balance check only for new applications — don't block sign/approve
|
||||
if (status == CitizenshipStatus.NOT_STARTED) {
|
||||
val freeBalance = pezkuwiDashboardRepository.queryFreeBalance(ChainGeneses.PEZKUWI_PEOPLE, accountId)
|
||||
if (freeBalance < MIN_BALANCE_PLANCK) {
|
||||
showError(resourceManager.getString(R.string.citizenship_insufficient_balance))
|
||||
_dismissEvent.postValue(Event(Unit))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
_citizenshipStatus.postValue(status)
|
||||
|
||||
if (status == CitizenshipStatus.APPROVED) {
|
||||
loadPendingApprovals(chain, accountId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load KYC status", e)
|
||||
_citizenshipStatus.postValue(CitizenshipStatus.NOT_STARTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryKycStatus(accountId: ByteArray): CitizenshipStatus {
|
||||
return try {
|
||||
pezkuwiDashboardRepository.queryKycStatus(ChainGeneses.PEZKUWI_PEOPLE, accountId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "queryKycStatus failed", e)
|
||||
CitizenshipStatus.NOT_STARTED
|
||||
}
|
||||
}
|
||||
|
||||
fun submitApplication(
|
||||
name: String,
|
||||
fatherName: String,
|
||||
grandfatherName: String,
|
||||
motherName: String,
|
||||
tribe: String,
|
||||
region: String,
|
||||
referrerAddress: String?
|
||||
) {
|
||||
launch {
|
||||
_isLoading.postValue(true)
|
||||
try {
|
||||
val chain = peopleChain ?: chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
val accountId = cachedAccountId ?: run {
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
metaAccount.accountIdIn(chain) ?: throw IllegalStateException("No account for People Chain")
|
||||
}
|
||||
|
||||
val jsonString = """{"name":"${name.trim().lowercase()}","email":"","documents":[]}"""
|
||||
val identityHash = keccak256(jsonString.toByteArray())
|
||||
|
||||
val referrerAccountId = referrerAddress?.let {
|
||||
try {
|
||||
it.toAccountId()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Invalid referrer address: $it", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val arguments = mutableMapOf<String, Any?>(
|
||||
"identity_hash" to identityHash,
|
||||
"referrer" to referrerAccountId
|
||||
)
|
||||
|
||||
val result = extrinsicService.submitExtrinsic(
|
||||
chain = chain,
|
||||
origin = TransactionOrigin.SelectedWallet
|
||||
) {
|
||||
call(
|
||||
moduleName = "IdentityKyc",
|
||||
callName = "apply_for_citizenship",
|
||||
arguments = arguments
|
||||
)
|
||||
}
|
||||
result.getOrThrow()
|
||||
|
||||
showToast(resourceManager.getString(R.string.citizenship_success))
|
||||
_citizenshipStatus.postValue(CitizenshipStatus.PENDING_REFERRAL)
|
||||
_dismissEvent.postValue(Event(Unit))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "submitApplication failed", e)
|
||||
showError(e)
|
||||
} finally {
|
||||
_isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun signApplication() {
|
||||
launch {
|
||||
_isLoading.postValue(true)
|
||||
try {
|
||||
val chain = peopleChain ?: chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
|
||||
val result = extrinsicService.submitExtrinsic(
|
||||
chain = chain,
|
||||
origin = TransactionOrigin.SelectedWallet
|
||||
) {
|
||||
call(
|
||||
moduleName = "IdentityKyc",
|
||||
callName = "confirm_citizenship",
|
||||
arguments = emptyMap()
|
||||
)
|
||||
}
|
||||
result.getOrThrow()
|
||||
|
||||
showToast(resourceManager.getString(R.string.citizenship_success))
|
||||
_citizenshipStatus.postValue(CitizenshipStatus.APPROVED)
|
||||
_dismissEvent.postValue(Event(Unit))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "signApplication failed", e)
|
||||
showError(e)
|
||||
} finally {
|
||||
_isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shareReferralLink() {
|
||||
launch {
|
||||
try {
|
||||
val chain = peopleChain ?: chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
val address = metaAccount.addressIn(chain) ?: return@launch
|
||||
|
||||
val shareText = resourceManager.getString(R.string.citizenship_share_referral, address)
|
||||
_shareEvent.postValue(Event(shareText))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "shareReferralLink failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun approveReferral(applicantAccountId: ByteArray) {
|
||||
launch {
|
||||
_isLoading.postValue(true)
|
||||
try {
|
||||
val chain = peopleChain ?: chainRegistry.getChain(ChainGeneses.PEZKUWI_PEOPLE)
|
||||
|
||||
val result = extrinsicService.submitExtrinsic(
|
||||
chain = chain,
|
||||
origin = TransactionOrigin.SelectedWallet
|
||||
) {
|
||||
call(
|
||||
moduleName = "IdentityKyc",
|
||||
callName = "approve_referral",
|
||||
arguments = mapOf("applicant" to applicantAccountId)
|
||||
)
|
||||
}
|
||||
result.getOrThrow()
|
||||
|
||||
showToast(resourceManager.getString(R.string.citizenship_approve_success))
|
||||
// Reload the list
|
||||
val accountId = cachedAccountId
|
||||
if (accountId != null) {
|
||||
loadPendingApprovals(chain, accountId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "approveReferral failed", e)
|
||||
showError(e)
|
||||
} finally {
|
||||
_isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadPendingApprovals(chain: Chain, referrerAccountId: ByteArray) {
|
||||
val approvals = pezkuwiDashboardRepository.getPendingApprovals(chain, referrerAccountId)
|
||||
Log.d(TAG, "Loaded ${approvals.size} pending approvals")
|
||||
_pendingApprovals.postValue(approvals)
|
||||
}
|
||||
|
||||
private fun keccak256(input: ByteArray): ByteArray {
|
||||
val digest = Keccak.Digest256()
|
||||
return digest.digest(input)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.citizenship.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_assets.presentation.citizenship.CitizenshipBottomSheet
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
CitizenshipModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface CitizenshipComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): CitizenshipComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: CitizenshipBottomSheet)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.citizenship.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
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.presentation.citizenship.CitizenshipViewModel
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import javax.inject.Named
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class CitizenshipModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun providePezkuwiDashboardRepository(
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageDataSource: StorageDataSource
|
||||
): PezkuwiDashboardRepository {
|
||||
return PezkuwiDashboardRepository(remoteStorageDataSource)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(CitizenshipViewModel::class)
|
||||
fun provideViewModel(
|
||||
extrinsicService: ExtrinsicService,
|
||||
chainRegistry: ChainRegistry,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
resourceManager: ResourceManager,
|
||||
pezkuwiDashboardRepository: PezkuwiDashboardRepository
|
||||
): ViewModel {
|
||||
return CitizenshipViewModel(
|
||||
extrinsicService = extrinsicService,
|
||||
chainRegistry = chainRegistry,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
resourceManager = resourceManager,
|
||||
pezkuwiDashboardRepository = pezkuwiDashboardRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory,
|
||||
): CitizenshipViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(CitizenshipViewModel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFFFFF" />
|
||||
<corners android:radius="4dp" />
|
||||
<padding
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="4dp"
|
||||
android:bottom="4dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,224 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/citizenshipTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/citizenship_title"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/citizenshipStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/citizenshipFormScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/citizenshipFormContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipNameLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_name"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipFatherNameLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_father_name"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipFatherNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipGrandfatherNameLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_grandfather_name"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipGrandfatherNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipMotherNameLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_mother_name"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipMotherNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipTribeLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_tribe"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipTribeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/citizenship_region"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/citizenshipRegionSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/bg_spinner_outlined"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/citizenshipReferrerLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/citizenship_referrer_hint"
|
||||
app:boxStrokeColor="@android:color/white"
|
||||
app:hintTextColor="@android:color/white">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/citizenshipReferrerInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:inputType="text"
|
||||
android:textSize="12sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/citizenshipProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/citizenshipActionButton"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/citizenship_apply"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/citizenshipShareButton"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/citizenship_share_button"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@android:color/white"
|
||||
app:strokeColor="@android:color/white"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Invitations list (visible only for citizens) -->
|
||||
<TextView
|
||||
android:id="@+id/citizenshipInvitationsHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/citizenshipInvitationsScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/citizenshipInvitationsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
@@ -96,25 +96,38 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Info text -->
|
||||
<TextView
|
||||
android:id="@+id/pezkuwiDashboardInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/pezkuwi_dashboard_info"
|
||||
android:textColor="#CFD8DC"
|
||||
android:textSize="11sp"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<!-- Action button -->
|
||||
<!-- Action buttons: Kesk (green), Sor (red), Zer (yellow) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pezkuwiDashboardBasvuruButton"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/pezkuwi_dashboard_basvuru"
|
||||
android:textAllCaps="false" />
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTint="#4CAF50"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pezkuwiDashboardSignButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/citizenship_sign"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTint="#E53935"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pezkuwiDashboardShareButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/citizenship_share_button"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#212121"
|
||||
app:backgroundTint="#FDD835"
|
||||
app:cornerRadius="8dp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
Reference in New Issue
Block a user