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:
2026-02-22 05:04:43 +03:00
parent f6dc35b80e
commit 771c9c8877
18 changed files with 1157 additions and 20 deletions
+28
View File
@@ -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>
+28
View File
@@ -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>
+28
View File
@@ -2767,4 +2767,32 @@
<string name="pezkuwi_dashboard_basvuru">Apply &amp; 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>
@@ -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
)
@@ -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
)
@@ -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
@@ -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)
}
}
@@ -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))
@@ -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 {
@@ -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
)
@@ -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>) {
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
@@ -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>